diff --git a/CLAUDE.md b/CLAUDE.md index 6963ee1f..6169c932 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,6 +154,48 @@ Existing data needs the one-off `scripts/migrate-is-owned-to-owned-versions.js` then a `scripts/mongodb-create-index.js` re-run to replace the old `is_owned` index definitions. Covered by the resource tests' owned-filter cases (including a full decimal/date round-trip in `MovieResourceTest`/`AlbumResourceTest`) and `OwnershipSmokeTest` (Playwright, end-to-end through the editor). +`VideoGamePlatformModel.ProductName` (free text) is the store's own specific product/edition text for that copy (e.g. "Grand Theft Auto V : Édition Premium"), distinct from the game's own `Title` - +added for the generic video game transaction import below, but it's a plain field on the shared model/entity/DTO, editable on `VideoGameDetail.razor` like any other copy detail regardless of how the platform entry was created. +It renders via a new `ExtraFields` render-fragment slot on the shared `OwnedVersionFields` component (`Components/Inventory/Shared/`), not by adding it to `IOwnedCopyDto`: +Movie/TvShow/Book/Album's `OwnedVersionDto` has no equivalent concept, so the interface every `OwnedVersionFields` caller shares stays free of a field only one of them needs. +The component's own Price/Acquired/Vendor/Reference columns switched from a fixed `col-md-3` to an unnumbered `col-md` (Bootstrap's equal-width auto layout) for this - +with no `ExtraFields` supplied they still fill one row identically to the old fixed split, +but a 5th `ExtraFields` column (VideoGame's Product field) joins the same row and every column re-shares the width automatically instead of wrapping to a second row on desktop, +while `col-6` still stacks two-per-row on mobile like the others. + +### Bulk store/retailer transaction imports (Amazon, generic video game transactions) + +`AmazonImportController`/`AmazonOrderPreviewService` (preview an uploaded order-history CSV, let the user pick a type and edit fields per row, then commit) was the first of this shape, +followed by `GenericVideoGameImportController`/`GenericVideoGameImportService` for a video-game-only transaction-history CSV (PSN's own GDPR export today; the format isn't PlayStation-specific - +`Vendor` is a per-row column, not hardcoded - so any store exporting the same shape would work). +Both follow `WatchNextController`/`WishlistController`'s split (controller in `Controllers/`, pure parsing/computation in `Domain/Services/`), not the older `WebApi/Import/` feature-folder shape. +The create/merge/dedup engine behind both (`ComputeCommitPlan`/`FindImportedReferences`, matching by normalized title via `TitleNormalizer`, +merging within the same commit batch too) started out Amazon-only (`AmazonImportMergeService`) but was already fully generic - it moved to `Domain/Services/OwnedItemImportMergeService.cs` +(and its return type to `Domain/Models/ImportCommitPlan.cs`, renamed off `AmazonImportPlan`) once the video game importer needed the exact same engine, rather than duplicating it. +`AmazonImportMergeService` kept only what's genuinely Amazon-specific: `FormatOrderReference` (ASIN + order id) and `BuildAmazonProvenanceNotes`. + +**Gotcha, confirmed against a real PSN export:** a transaction/order id pair is *not* reliably unique per line the way Amazon's ASIN is. +A single PSN transaction can bundle several different products under one shared Transaction Id/Order Id (confirmed with a real export: +one transaction contained three separate "Far Cry 4" DLC packs, each its own CSV line, distinguishable only by Product Name). +`GenericVideoGameImportService.FormatReference` originally built the owned-copy `Reference` (and dedup key) from transaction id + order id alone, +the same class of bug `AmazonImportMergeService.FormatOrderReference` already avoids by including the ASIN - +without a per-product disambiguator, the second and third bundled lines collided with the first's reference and were silently skipped as "already imported" duplicates, +so only 1 of the 3 platform entries actually got created from a 3-line selection. +Fixed by appending the row's own Product Name (falling back to the title when blank) to the reference text, mirroring the ASIN's role for Amazon. +`GenericVideoGameImportPreviewRow.RowId` had the identical problem (plain `TransactionId`, colliding across the same bundled lines) and got the same fix. +Covered by `GenericVideoGameImportServiceTest.FormatReference_Disambiguates_WhenTwoLinesShareTheSameTransactionAndOrder` +and `GenericVideoGameImportResourceTest.PreviewThenCommit_ImportsAllThreeLines_WhenTheyShareOneTransactionAndOrderButDifferentProducts`. + +**`VideoGamesCreated`/`VideoGamesMergedInto` count distinct video games, not selected rows - this undercounts rows on purpose, not a bug.** +Several selected rows sharing a normalized title consolidate into one item within a single commit batch (`ComputeCommitPlan`'s existing same-batch-merge behavior), +which is correct but means `Created + MergedInto` can come out lower than the number of rows the user selected, +which read as a discrepancy on first use of the real 104-row PSN export (98 selected, "1 created, 0 merged, 2 already imported" summed to only 3 - actually the *bundling* bug above, +but the created/merged-vs-selected gap is real and independent of it). +`ImportCommitPlan.OwnedCopiesAdded`/`GenericVideoGameImportCommitResultDto.RowsImported` is the true per-row count (`RowsImported + VideoGamesSkipped` always equals the number of rows submitted), +and `SkippedRowTitles` lists exactly which selected rows were skipped as already-imported duplicates - +together they let `GenericVideoGameImportPage.razor` show a reconciling "X of Y selected rows imported" line plus a named list of anything actually skipped, +so the user can trust nothing was silently dropped instead of having to guess from the per-item counts alone. + ### Child entities (1-to-many owned by another entity) `CarHistory` (owned by `Car`) and `Episode` (owned by `TvShow`) are separate top-level collections referencing their parent by id (`car_id`, `tv_show_id`), not embedded arrays. diff --git a/Directory.Build.props b/Directory.Build.props index d4cacdfb..0a5f0828 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ - 2.3.0 + 2.4.0 diff --git a/docs/prerender-flash-fix.md b/docs/prerender-flash-fix.md index 63137a6d..1c3901fe 100644 --- a/docs/prerender-flash-fix.md +++ b/docs/prerender-flash-fix.md @@ -113,6 +113,14 @@ Two real options, and I have a clear preference: The detail pages persist a single bounded DTO and are fine to keep as-is. 2. Raise the limit via .AddHubOptions(o => o.MaximumReceiveMessageSize = 256 * 1024) — one line, keeps the no-refetch behavior, but the payload still grows with your collection and the cliff just moves further out. +**Update**: "the detail pages persist a single bounded DTO" turned out not to hold for TvShowDetail. +`TvShowReferenceModel.Episodes` is embedded (see CLAUDE.md's "Reference data" section) and unbounded per show, +so `[PersistentState]`-serializing `Reference` plus the raw `Episodes` list hit the exact same 32 KB SignalR ceiling for shows with many seasons/episodes. +`[PersistentState]` was reverted from `TvShowDetail.razor` +(back to plain private fields `_show`/`_reference`/`_episodes`, `OnParametersSetAsync` always calls `LoadAsync`, no restore-skip path) for the same reason it was dropped from Watch Next/Wishlist above. +The `_loaded`/`_loading` flash fix from "Fix loading flash" stays — it isn't part of the persisted-state mechanism and never caused this issue. +Movie/Book/Album/VideoGame/Car/House/HealthProfile/Playlist detail pages keep `[PersistentState]` since their persisted DTOs stay genuinely bounded (no embedded unbounded list like `Episodes`). + Sources: [dotnet/aspnetcore#65101](https://github.com/dotnet/aspnetcore/issues/65101), [Blazor SignalR guidance — message size](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/signalr), diff --git a/samples/angular/.editorconfig b/samples/angular/.editorconfig deleted file mode 100644 index d0dcd4a4..00000000 --- a/samples/angular/.editorconfig +++ /dev/null @@ -1,14 +0,0 @@ -# Editor configuration, see http://editorconfig.org -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -insert_final_newline = true -trim_trailing_whitespace = true -end_of_line = lf - -[*.md] -max_line_length = off -trim_trailing_whitespace = false diff --git a/samples/angular/.eslintrc.json b/samples/angular/.eslintrc.json deleted file mode 100644 index 0ac5f715..00000000 --- a/samples/angular/.eslintrc.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "root": true, - "ignorePatterns": [ - "dist", - "results", - "coverage" - ], - "parserOptions": { - "ecmaVersion": 2020 - }, - "overrides": [ - { - "files": [ - "*.ts" - ], - "parserOptions": { - "project": [ - "tsconfig.json", - "e2e/tsconfig.json" - ], - "createDefaultProgram": true - }, - "extends": [ - "plugin:@angular-eslint/recommended", - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/consistent-type-definitions": "error", - "@typescript-eslint/dot-notation": "off", - "@typescript-eslint/explicit-member-accessibility": [ - "off", - { - "accessibility": "explicit" - } - ], - "@typescript-eslint/naming-convention": "warn", - "brace-style": [ - "error", - "1tbs" - ], - "id-blacklist": "off", - "id-match": "off", - "no-underscore-dangle": "off" - } - }, - { - "files": [ - "*.html" - ], - "extends": [ - "plugin:@angular-eslint/template/recommended" - ], - "rules": {} - } - ] -} diff --git a/samples/angular/.gitignore b/samples/angular/.gitignore deleted file mode 100644 index af4a6f0d..00000000 --- a/samples/angular/.gitignore +++ /dev/null @@ -1,55 +0,0 @@ -# See http://help.github.com/ignore-files/ for more about ignoring files. - -# compiled output -/dist -/dist-server -/tmp -/out-tsc - -# dependencies -/node_modules - -# IDEs and editors -/.idea -.project -.classpath -.c9/ -*.launch -.settings/ -*.sublime-workspace - -# IDE - VSCode -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json - -# misc -/.angular/cache -/.sass-cache -/connect.lock -/coverage -/libpeerconnection.log -npm-debug.log -yarn-error.log -testem.log -/typings - -# System Files -.DS_Store -Thumbs.db - -# Firebase -.firebaserc - -# Angular development config -environment.dev.ts -environment.prod.ts - -# Temporary files -/temp.txt - -# Test report files -/TESTS*.xml -/coverage diff --git a/samples/angular/README.md b/samples/angular/README.md deleted file mode 100644 index 520fe3fb..00000000 --- a/samples/angular/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Keeptrack Angular frontend application - -This project was generated with [Angular CLI](https://github.com/angular/angular-cli). - -## Local setup - -### Requirements - -* [NPM (Node.js)](https://nodejs.org/en/) -* [Angular CLI](https://cli.angular.io/): `npm install -g @angular/cli` -* [Firebase CLI](https://www.npmjs.com/package/firebase-tools): `npm install -g firebase-tools` - -### Configuration - -* Create a file `src/environments/environment.dev.ts` and update it (this file is not purposed to node save secret values) - * Open your project in the [Firebase console](https://console.firebase.google.com/), go in the properties pages (General tab), inside "Your apps" part you'll have all Firebase information - * Get the value from your local API URL (`https://localhost:5011`) - -### Installation of package - -Run `npm install` to load all dependencies. - -### Development server - -Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. - -_Warning_: you may encounter CORS issues while testing with Firefox, use Chrome for local debug - -### Code scaffolding - -Run `ng generate component / --module=` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. - -### Build - -Run `npm run build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. - -### Running unit tests - -Run `npm run test` to execute the unit tests via [Karma](https://karma-runner.github.io). - -### Running end-to-end tests - -Run `npm run e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). - -### Running linter - -Run `npm run lint` to validate source code standards. - -### Further help - -To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). - -### Update procedure - -* Update NPM: `npm install --global npm` -* Follow procedure described by [update.angular.io](https://update.angular.io/): `ng update ` - -### Firebase guide - -* [Getting started with Firebase Authentication](https://github.com/angular/angularfire/blob/master/docs/auth/getting-started.md) -* [angularfire/samples/compat](https://github.com/angular/angularfire/tree/master/samples/compat) - -### Graphic design - -* [Bootstrap v4.6](https://getbootstrap.com/docs/4.6/getting-started/introduction/) -* [Font Awesome v4.7.0](https://fontawesome.bootstrapcheatsheets.com/) diff --git a/samples/angular/angular.json b/samples/angular/angular.json deleted file mode 100644 index 52a102ca..00000000 --- a/samples/angular/angular.json +++ /dev/null @@ -1,191 +0,0 @@ -{ - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "version": 1, - "newProjectRoot": "projects", - "projects": { - "AngularWebApp": { - "projectType": "application", - "schematics": { - "@schematics/angular:component": { - "standalone": true - }, - "@schematics/angular:directive": { - "standalone": true - }, - "@schematics/angular:pipe": { - "standalone": true - } - }, - "root": "", - "sourceRoot": "src", - "prefix": "app", - "architect": { - "build": { - "builder": "@angular/build:application", - "options": { - "progress": false, - "outputPath": { - "base": "dist" - }, - "index": "src/index.html", - "polyfills": [ - "@angular/localize/init", - "zone.js" - ], - "tsConfig": "tsconfig.app.json", - "assets": [ - "src/assets", - "src/web.config" - ], - "styles": [ - "node_modules/bootstrap/dist/css/bootstrap.min.css", - "node_modules/font-awesome/css/font-awesome.css", - "src/styles.css" - ], - "scripts": [], - "extractLicenses": false, - "sourceMap": true, - "optimization": false, - "namedChunks": true, - "browser": "src/main.ts" - }, - "configurations": { - "dev": { - "budgets": [ - { - "type": "anyComponentStyle", - "maximumWarning": "6kb" - } - ], - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.dev.ts" - } - ] - }, - "production": { - "budgets": [ - { - "type": "anyComponentStyle", - "maximumWarning": "6kb" - } - ], - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.prod.ts" - } - ], - "optimization": true, - "outputHashing": "all", - "sourceMap": false, - "namedChunks": false, - "extractLicenses": true - } - }, - "defaultConfiguration": "" - }, - "serve": { - "builder": "@angular/build:dev-server", - "options": { - "buildTarget": "AngularWebApp:build" - }, - "configurations": { - "dev": { - "buildTarget": "AngularWebApp:build:dev" - }, - "production": { - "buildTarget": "AngularWebApp:build:production" - } - } - }, - "extract-i18n": { - "builder": "@angular/build:extract-i18n", - "options": { - "buildTarget": "AngularWebApp:build" - } - }, - "test": { - "builder": "@angular/build:karma", - "options": { - "polyfills": [ - "@angular/localize/init", - "zone.js", - "zone.js/testing" - ], - "tsConfig": "tsconfig.spec.json", - "karmaConfig": "karma.conf.js", - "codeCoverageExclude": [ - "**/models/*.ts", - "**/app.module.ts", - "**/app.routes.ts" - ], - "assets": [ - "src/favicon.ico", - "src/assets" - ], - "styles": [ - "src/styles.css" - ], - "scripts": [] - }, - "configurations": { - "dev": { - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.dev.ts" - } - ] - } - } - }, - "lint": { - "builder": "@angular-eslint/builder:lint", - "options": { - "lintFilePatterns": [ - "src/**/*.ts", - "src/**/*.html" - ] - } - } - } - } - }, - "schematics": { - "@angular-eslint/schematics:application": { - "setParserOptionsProject": true - }, - "@angular-eslint/schematics:library": { - "setParserOptionsProject": true - }, - "@schematics/angular:component": { - "type": "component" - }, - "@schematics/angular:directive": { - "type": "directive" - }, - "@schematics/angular:service": { - "type": "service" - }, - "@schematics/angular:guard": { - "typeSeparator": "." - }, - "@schematics/angular:interceptor": { - "typeSeparator": "." - }, - "@schematics/angular:module": { - "typeSeparator": "." - }, - "@schematics/angular:pipe": { - "typeSeparator": "." - }, - "@schematics/angular:resolver": { - "typeSeparator": "." - } - }, - "cli": { - "analytics": false - } -} diff --git a/samples/angular/firebase.json b/samples/angular/firebase.json deleted file mode 100644 index 189585aa..00000000 --- a/samples/angular/firebase.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "hosting": [ - { - "target": "AngularWebApp", - "public": "dist", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ], - "rewrites": [ - { - "source": "**", - "destination": "/index.html" - } - ] - } - ] -} diff --git a/samples/angular/karma.conf.js b/samples/angular/karma.conf.js deleted file mode 100644 index fc4f986d..00000000 --- a/samples/angular/karma.conf.js +++ /dev/null @@ -1,37 +0,0 @@ -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-coverage'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-junit-reporter') - ], - client: { - jasmine: {}, - clearContext: false // leave Jasmine Spec Runner output visible in browser - }, - preprocessors: { - 'src/**/*.js': ['coverage'] - }, - coverageReporter: { - dir: require('path').join(__dirname, '/coverage'), - reporters: [ - { type: 'html', subdir: 'html' }, - { type: 'lcovonly', subdir: '.', file: 'lcov.info' } - ] - }, - junitReporter: { - outputDir: require('path').join(__dirname, '.'), - }, - reporters: ['progress', 'kjhtml', 'junit', 'coverage'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], // 'Chrome', 'ChromeHeadless' - singleRun: false - }); -}; diff --git a/samples/angular/package-lock.json b/samples/angular/package-lock.json deleted file mode 100644 index b6a9c239..00000000 --- a/samples/angular/package-lock.json +++ /dev/null @@ -1,12309 +0,0 @@ -{ - "name": "keeptrack-angularwebapp", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "keeptrack-angularwebapp", - "version": "0.1.0", - "dependencies": { - "@angular/animations": "^21.2.1", - "@angular/common": "^21.2.1", - "@angular/compiler": "^21.2.1", - "@angular/core": "^21.2.1", - "@angular/fire": "^21.0.0-rc.0", - "@angular/forms": "^21.2.1", - "@angular/localize": "^21.2.1", - "@angular/platform-browser": "^21.2.1", - "@angular/platform-browser-dynamic": "^21.2.1", - "@angular/platform-server": "^21.2.1", - "@angular/router": "^21.2.1", - "@popperjs/core": "~2.11.8", - "bootstrap": "~5.3.2", - "core-js": "^3.48.0", - "font-awesome": "^4.7.0", - "rxjs": "~7.8.2", - "tslib": "^2.8.1", - "zone.js": "~0.16.1" - }, - "devDependencies": { - "@angular-devkit/architect": "^0.2102.1", - "@angular-eslint/builder": "^21.3.0", - "@angular-eslint/eslint-plugin": "^21.3.0", - "@angular-eslint/eslint-plugin-template": "^21.3.0", - "@angular-eslint/schematics": "^21.3.0", - "@angular-eslint/template-parser": "^21.3.0", - "@angular/build": "^21.2.1", - "@angular/cli": "^21.2.1", - "@angular/compiler-cli": "^21.2.1", - "@angular/language-service": "^21.2.1", - "@types/jasmine": "~6.0.0", - "@types/node": "^22.13.14", - "acorn": "~8.16.0", - "eslint": "^10.0.3", - "fuzzy": "~0.1.3", - "glob": "^13.0.6", - "jasmine": "~6.1.0", - "jasmine-core": "~6.1.0", - "jasmine-spec-reporter": "~7.0.0", - "karma": "~6.4.4", - "karma-chrome-launcher": "~3.2.0", - "karma-coverage": "~2.2.1", - "karma-jasmine": "~5.1.0", - "karma-jasmine-html-reporter": "~2.2.0", - "karma-junit-reporter": "~2.0.1", - "typescript": "~5.9.3" - }, - "optionalDependencies": { - "ts-node": "~10.7.0" - } - }, - "node_modules/@algolia/abtesting": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.1.tgz", - "integrity": "sha512-Dkj0BgPiLAaim9sbQ97UKDFHJE/880wgStAM18U++NaJ/2Cws34J5731ovJifr6E3Pv4T2CqvMXf8qLCC417Ew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-abtesting": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.48.1.tgz", - "integrity": "sha512-LV5qCJdj+/m9I+Aj91o+glYszrzd7CX6NgKaYdTOj4+tUYfbS62pwYgUfZprYNayhkQpVFcrW8x8ZlIHpS23Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-analytics": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.48.1.tgz", - "integrity": "sha512-/AVoMqHhPm14CcHq7mwB+bUJbfCv+jrxlNvRjXAuO+TQa+V37N8k1b0ijaRBPdmSjULMd8KtJbQyUyabXOu6Kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-common": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.48.1.tgz", - "integrity": "sha512-VXO+qu2Ep6ota28ktvBm3sG53wUHS2n7bgLWmce5jTskdlCD0/JrV4tnBm1l7qpla1CeoQb8D7ShFhad+UoSOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-insights": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.48.1.tgz", - "integrity": "sha512-zl+Qyb0nLg+Y5YvKp1Ij+u9OaPaKg2/EPzTwKNiVyOHnQJlFxmXyUZL1EInczAZsEY8hVpPCLtNfhMhfxluXKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-personalization": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.48.1.tgz", - "integrity": "sha512-r89Qf9Oo9mKWQXumRu/1LtvVJAmEDpn8mHZMc485pRfQUMAwSSrsnaw1tQ3sszqzEgAr1c7rw6fjBI+zrAXTOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-query-suggestions": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.48.1.tgz", - "integrity": "sha512-TPKNPKfghKG/bMSc7mQYD9HxHRUkBZA4q1PEmHgICaSeHQscGqL4wBrKkhfPlDV1uYBKW02pbFMUhsOt7p4ZpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-search": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.1.tgz", - "integrity": "sha512-4Fu7dnzQyQmMFknYwTiN/HxPbH4DyxvQ1m+IxpPp5oslOgz8m6PG5qhiGbqJzH4HiT1I58ecDiCAC716UyVA8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/ingestion": { - "version": "1.48.1", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.48.1.tgz", - "integrity": "sha512-/RFq3TqtXDUUawwic/A9xylA2P3LDMO8dNhphHAUOU51b1ZLHrmZ6YYJm3df1APz7xLY1aht6okCQf+/vmrV9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/monitoring": { - "version": "1.48.1", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.48.1.tgz", - "integrity": "sha512-Of0jTeAZRyRhC7XzDSjJef0aBkgRcvRAaw0ooYRlOw57APii7lZdq+layuNdeL72BRq1snaJhoMMwkmLIpJScw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/recommend": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.48.1.tgz", - "integrity": "sha512-bE7JcpFXzxF5zHwj/vkl2eiCBvyR1zQ7aoUdO+GDXxGp0DGw7nI0p8Xj6u8VmRQ+RDuPcICFQcCwRIJT5tDJFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/requester-browser-xhr": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.48.1.tgz", - "integrity": "sha512-MK3wZ2koLDnvH/AmqIF1EKbJlhRS5j74OZGkLpxI4rYvNi9Jn/C7vb5DytBnQ4KUWts7QsmbdwHkxY5txQHXVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.48.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/requester-fetch": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.48.1.tgz", - "integrity": "sha512-2oDT43Y5HWRSIQMPQI4tA/W+TN/N2tjggZCUsqQV440kxzzoPGsvv9QP1GhQ4CoDa+yn6ygUsGp6Dr+a9sPPSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.48.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/requester-node-http": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.48.1.tgz", - "integrity": "sha512-xcaCqbhupVWhuBP1nwbk1XNvwrGljozutEiLx06mvqDf3o8cHyEgQSHS4fKJM+UAggaWVnnFW+Nne5aQ8SUJXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.48.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@angular-devkit/architect": { - "version": "0.2102.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.1.tgz", - "integrity": "sha512-x2Qqz6oLYvEh9UBUG0AP1A4zROO/VP+k+zM9+4c2uZw1uqoBQFmutqgzncjVU7cR9R0RApgx9JRZHDFtQru68w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "21.2.1", - "rxjs": "7.8.2" - }, - "bin": { - "architect": "bin/cli.js" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular-devkit/core": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.1.tgz", - "integrity": "sha512-TpXGjERqVPN8EPt7LdmWAwh0oNQ/6uWFutzGZiXhJy81n1zb1O1XrqhRAmvP1cAo5O+na6IV2JkkCmxL6F8GUg==", - "license": "MIT", - "dependencies": { - "ajv": "8.18.0", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.3", - "rxjs": "7.8.2", - "source-map": "0.7.6" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^5.0.0" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/schematics": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.1.tgz", - "integrity": "sha512-CWoamHaasAHMjHcYqxbj0tMnoXxdGotcAz2SpiuWtH28Lnf5xfbTaJn/lwdMP8Wdh4tgA+uYh2l45A5auCwmkw==", - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "21.2.1", - "jsonc-parser": "3.3.1", - "magic-string": "0.30.21", - "ora": "9.3.0", - "rxjs": "7.8.2" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular-eslint/builder": { - "version": "21.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.3.0.tgz", - "integrity": "sha512-26QUUouei52biUFAlJSrWNAU9tuF2miKwd8uHdxWwCF31xz+OxC5+NfudWvt1AFaYow7gWueX1QX3rNNtSPDrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/architect": ">= 0.2100.0 < 0.2200.0", - "@angular-devkit/core": ">= 21.0.0 < 22.0.0" - }, - "peerDependencies": { - "@angular/cli": ">= 21.0.0 < 22.0.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": "*" - } - }, - "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "21.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.3.0.tgz", - "integrity": "sha512-l521I24J9gJxyMbRkrM24Tc7W8J8BP+TDAmVs2nT8+lXbS3kg8QpWBRtd+hNUgq6o+vt+lKBkytnEfu8OiqeRg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular-eslint/eslint-plugin": { - "version": "21.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.3.0.tgz", - "integrity": "sha512-Whf/AUUBekOlfSJRS78m76YGrBQAZ3waXE7oOdlW5xEQvn8jBDN9EGuNnjg/syZzvzjK4ZpYC4g1XYXrc+fQIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.3.0", - "@angular-eslint/utils": "21.3.0", - "ts-api-utils": "^2.1.0" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": "*" - } - }, - "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "21.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.3.0.tgz", - "integrity": "sha512-lVixd/KypPWgA/5/pUOhJV9MTcaHjYZEqyOi+IiLk+h+maGxn6/s6Ot+20n+XGS85zAgOY+qUw6EEQ11hoojIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.3.0", - "@angular-eslint/utils": "21.3.0", - "aria-query": "5.3.2", - "axobject-query": "4.1.0" - }, - "peerDependencies": { - "@angular-eslint/template-parser": "21.3.0", - "@typescript-eslint/types": "^7.11.0 || ^8.0.0", - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": "*" - } - }, - "node_modules/@angular-eslint/schematics": { - "version": "21.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.3.0.tgz", - "integrity": "sha512-8deU/zVY9f8k8kAQQ9PL130ox2VlrZw3fMxgsPNAY5tjQ0xk0J2YVSszYHhcqdMGG1J01IsxIjvQaJ4pFfEmMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": ">= 21.0.0 < 22.0.0", - "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", - "@angular-eslint/eslint-plugin": "21.3.0", - "@angular-eslint/eslint-plugin-template": "21.3.0", - "ignore": "7.0.5", - "semver": "7.7.4", - "strip-json-comments": "3.1.1" - }, - "peerDependencies": { - "@angular/cli": ">= 21.0.0 < 22.0.0" - } - }, - "node_modules/@angular-eslint/template-parser": { - "version": "21.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.3.0.tgz", - "integrity": "sha512-ysyou1zAY6M6rSZNdIcYKGd4nk6TCapamyFNB3ivmTlVZ0O35TS9o/rJ0aUttuHgDp+Ysgs3ql+LA746PXgCyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.3.0", - "eslint-scope": "^9.1.1" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": "*" - } - }, - "node_modules/@angular-eslint/utils": { - "version": "21.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.3.0.tgz", - "integrity": "sha512-oNigH6w3l+owTMboj/uFG0tHOy43uH8BpQRtBOQL1/s2+5in/BJ2Fjobv3SyizxTgeJ1FhRefbkT8GmVjK7jAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.3.0" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": "*" - } - }, - "node_modules/@angular/animations": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.1.tgz", - "integrity": "sha512-zT/S29pUTbziCLvZ2itBdNWd5i8tsXexofH7KA4n2yvYmK1EhNpE7TlHRjghmsHgtDt4VnGiMW4zXEyrl05Dwg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/core": "21.2.1" - } - }, - "node_modules/@angular/build": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.1.tgz", - "integrity": "sha512-cUpLNHJp9taII/FOcJHHfQYlMcZSRaf6eIxgSNS6Xfx1CeGoJNDN+J8+GFk+H1CPJt1EvbfyZ+dE5DbsgTD/QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2102.1", - "@babel/core": "7.29.0", - "@babel/helper-annotate-as-pure": "7.27.3", - "@babel/helper-split-export-declaration": "7.24.7", - "@inquirer/confirm": "5.1.21", - "@vitejs/plugin-basic-ssl": "2.1.4", - "beasties": "0.4.1", - "browserslist": "^4.26.0", - "esbuild": "0.27.3", - "https-proxy-agent": "7.0.6", - "istanbul-lib-instrument": "6.0.3", - "jsonc-parser": "3.3.1", - "listr2": "9.0.5", - "magic-string": "0.30.21", - "mrmime": "2.0.1", - "parse5-html-rewriting-stream": "8.0.0", - "picomatch": "4.0.3", - "piscina": "5.1.4", - "rolldown": "1.0.0-rc.4", - "sass": "1.97.3", - "semver": "7.7.4", - "source-map-support": "0.5.21", - "tinyglobby": "0.2.15", - "undici": "7.22.0", - "vite": "7.3.1", - "watchpack": "2.5.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "optionalDependencies": { - "lmdb": "3.5.1" - }, - "peerDependencies": { - "@angular/compiler": "^21.0.0", - "@angular/compiler-cli": "^21.0.0", - "@angular/core": "^21.0.0", - "@angular/localize": "^21.0.0", - "@angular/platform-browser": "^21.0.0", - "@angular/platform-server": "^21.0.0", - "@angular/service-worker": "^21.0.0", - "@angular/ssr": "^21.2.1", - "karma": "^6.4.0", - "less": "^4.2.0", - "ng-packagr": "^21.0.0", - "postcss": "^8.4.0", - "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", - "tslib": "^2.3.0", - "typescript": ">=5.9 <6.0", - "vitest": "^4.0.8" - }, - "peerDependenciesMeta": { - "@angular/core": { - "optional": true - }, - "@angular/localize": { - "optional": true - }, - "@angular/platform-browser": { - "optional": true - }, - "@angular/platform-server": { - "optional": true - }, - "@angular/service-worker": { - "optional": true - }, - "@angular/ssr": { - "optional": true - }, - "karma": { - "optional": true - }, - "less": { - "optional": true - }, - "ng-packagr": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tailwindcss": { - "optional": true - }, - "vitest": { - "optional": true - } - } - }, - "node_modules/@angular/cli": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.1.tgz", - "integrity": "sha512-5SRfMTgwFj1zXOpfeZWHsxZBni0J4Xz7/CbewG47D6DmbstOrSdgt6eNzJ62R650t0G9dpri2YvToZgImtbjOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/architect": "0.2102.1", - "@angular-devkit/core": "21.2.1", - "@angular-devkit/schematics": "21.2.1", - "@inquirer/prompts": "7.10.1", - "@listr2/prompt-adapter-inquirer": "3.0.5", - "@modelcontextprotocol/sdk": "1.26.0", - "@schematics/angular": "21.2.1", - "@yarnpkg/lockfile": "1.1.0", - "algoliasearch": "5.48.1", - "ini": "6.0.0", - "jsonc-parser": "3.3.1", - "listr2": "9.0.5", - "npm-package-arg": "13.0.2", - "pacote": "21.3.1", - "parse5-html-rewriting-stream": "8.0.0", - "semver": "7.7.4", - "yargs": "18.0.0", - "zod": "4.3.6" - }, - "bin": { - "ng": "bin/ng.js" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/common": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.1.tgz", - "integrity": "sha512-xhv2i1Q9s1kpGbGsfj+o36+XUC/TQLcZyRuRxn3GwaN7Rv34FabC88ycpvoE+sW/txj4JRx9yPA0dRSZjwZ+Gg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/core": "21.2.1", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/compiler": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.1.tgz", - "integrity": "sha512-FxWaSaii1vfHIFA+JksqQ8NGB2frfqCrs7Ju50a44kbwR4fmanfn/VsiS/CbwBp9vcyT/Br9X/jAG4RuK/U2nw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@angular/compiler-cli": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.1.tgz", - "integrity": "sha512-qYCWLGtEju4cDtYLi4ZzbwKoF0lcGs+Lc31kuESvAzYvWNgk2EUOtwWo8kbgpAzAwSYodtxW6Q90iWEwfU6elw==", - "license": "MIT", - "dependencies": { - "@babel/core": "7.29.0", - "@jridgewell/sourcemap-codec": "^1.4.14", - "chokidar": "^5.0.0", - "convert-source-map": "^1.5.1", - "reflect-metadata": "^0.2.0", - "semver": "^7.0.0", - "tslib": "^2.3.0", - "yargs": "^18.0.0" - }, - "bin": { - "ng-xi18n": "bundles/src/bin/ng_xi18n.js", - "ngc": "bundles/src/bin/ngc.js" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/compiler": "21.2.1", - "typescript": ">=5.9 <6.1" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@angular/core": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.1.tgz", - "integrity": "sha512-pFTbg03s2ZI5cHNT+eWsGjwIIKiYkeAnodFbCAHjwFi9KCEYlTykFLjr9lcpGrBddfmAH7GE08Q73vgmsdcNHw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/compiler": "21.2.1", - "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.15.0 || ~0.16.0" - }, - "peerDependenciesMeta": { - "@angular/compiler": { - "optional": true - }, - "zone.js": { - "optional": true - } - } - }, - "node_modules/@angular/fire": { - "version": "21.0.0-rc.0-canary.ac3dd7c", - "resolved": "https://registry.npmjs.org/@angular/fire/-/fire-21.0.0-rc.0-canary.ac3dd7c.tgz", - "integrity": "sha512-Z1T9FrA8pd4JxUdQt1kDpsPGuXmqJSofsyiIZEJWboG4K1rezqN2JsB3cVGucIJ9uLljaFdvAtXYc+unBmAADQ==", - "license": "MIT", - "dependencies": { - "@angular-devkit/schematics": "^21.0.0", - "@schematics/angular": "^21.0.0", - "firebase": "^12.4.0", - "rxfire": "^6.1.0", - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/common": "^21.0.0", - "@angular/core": "^21.0.0", - "@angular/platform-browser": "^21.0.0", - "@angular/platform-browser-dynamic": "^21.0.0", - "@angular/platform-server": "^21.0.0", - "firebase-tools": "^14.0.0", - "rxjs": "~7.8.0" - }, - "peerDependenciesMeta": { - "@angular/platform-server": { - "optional": true - }, - "firebase-tools": { - "optional": true - } - } - }, - "node_modules/@angular/fire/node_modules/@firebase/ai": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.9.0.tgz", - "integrity": "sha512-NPvBBuvdGo9x3esnABAucFYmqbBmXvyTMimBq2PCuLZbdANZoHzGlx7vfzbwNDaEtCBq4RGGNMliLIv6bZ+PtA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check-interop-types": "0.3.3", - "@firebase/component": "0.7.1", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/analytics": { - "version": "0.10.20", - "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.20.tgz", - "integrity": "sha512-adGTNVUWH5q66tI/OQuKLSN6mamPpfYhj0radlH2xt+3eL6NFPtXoOs+ulvs+UsmK27vNFx5FjRDfWk+TyduHg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/installations": "0.6.20", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/analytics-compat": { - "version": "0.2.26", - "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.26.tgz", - "integrity": "sha512-0j2ruLOoVSwwcXAF53AMoniJKnkwiTjGVfic5LDzqiRkR13vb5j6TXMeix787zbLeQtN/m1883Yv1TxI0gItbA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/analytics": "0.10.20", - "@firebase/analytics-types": "0.8.3", - "@firebase/component": "0.7.1", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/app": { - "version": "0.14.9", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.9.tgz", - "integrity": "sha512-3gtUX0e584MYkKBQMgSECMvE1Dwzg+eONefDQ0wxVSe5YMBsZwdN5pL7UapwWBlV8+i8QCztF9TP947tEjZAGA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.0", - "idb": "7.1.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/app-check": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.1.tgz", - "integrity": "sha512-gmKfwQ2k8aUQlOyRshc+fOQLq0OwUmibIZvpuY1RDNu2ho0aTMlwxOuEiJeYOs7AxzhSx7gnXPFNsXCFbnvXUQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/app-check-compat": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.1.tgz", - "integrity": "sha512-yjSvSl5B1u4CirnxhzirN1uiTRCRfx+/qtfbyeyI+8Cx8Cw1RWAIO/OqytPSVwLYbJJ1vEC3EHfxazRaMoWKaA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check": "0.11.1", - "@firebase/app-check-types": "0.5.3", - "@firebase/component": "0.7.1", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/app-compat": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.9.tgz", - "integrity": "sha512-e5LzqjO69/N2z7XcJeuMzIp4wWnW696dQeaHAUpQvGk89gIWHAIvG6W+mA3UotGW6jBoqdppEJ9DnuwbcBByug==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app": "0.14.9", - "@firebase/component": "0.7.1", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/auth": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.1.tgz", - "integrity": "sha512-nXKj7d5bMBlnq6XpcQQpmnSVwEeHBkoVbY/+Wk0P1ebLSICoH4XPtvKOFlXKfIHmcS84mLQ99fk3njlDGKSDtw==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.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/@angular/fire/node_modules/@firebase/auth-compat": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.3.tgz", - "integrity": "sha512-nHOkupcYuGVxI1AJJ/OBhLPaRokbP14Gq4nkkoVvf1yvuREEWqdnrYB/CdsSnPxHMAnn5wJIKngxBF9jNX7s/Q==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/auth": "1.12.1", - "@firebase/auth-types": "0.13.0", - "@firebase/component": "0.7.1", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/component": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.1.tgz", - "integrity": "sha512-mFzsm7CLHR60o08S23iLUY8m/i6kLpOK87wdEFPLhdlCahaxKmWOwSVGiWoENYSmFJJoDhrR3gKSCxz7ENdIww==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/data-connect": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.4.0.tgz", - "integrity": "sha512-vLXM6WHNIR3VtEeYNUb/5GTsUOyl3Of4iWNZHBe1i9f88sYFnxybJNWVBjvJ7flhCyF8UdxGpzWcUnv6F5vGfg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.7.1", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/database": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.1.tgz", - "integrity": "sha512-LwIXe8+mVHY5LBPulWECOOIEXDiatyECp/BOlu0gOhe+WOcKjWHROaCbLlkFTgHMY7RHr5MOxkLP/tltWAH3dA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check-interop-types": "0.3.3", - "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.7.1", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.0", - "faye-websocket": "0.11.4", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/database-compat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.1.tgz", - "integrity": "sha512-heAEVZ9Z8c8PnBUcmGh91JHX0cXcVa1yESW/xkLuwaX7idRFyLiN8sl73KXpR8ZArGoPXVQDanBnk6SQiekRCQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/database": "1.1.1", - "@firebase/database-types": "1.0.17", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/database-types": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.17.tgz", - "integrity": "sha512-4eWaM5fW3qEIHjGzfi3cf0Jpqi1xQsAdT6rSDE1RZPrWu8oGjgrq6ybMjobtyHQFgwGCykBm4YM89qDzc+uG/w==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-types": "0.9.3", - "@firebase/util": "1.14.0" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/firestore": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.12.0.tgz", - "integrity": "sha512-PM47OyiiAAoAMB8kkq4Je14mTciaRoAPDd3ng3Ckqz9i2TX9D9LfxIRcNzP/OxzNV4uBKRq6lXoOggkJBQR3Gw==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.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/@angular/fire/node_modules/@firebase/firestore-compat": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.6.tgz", - "integrity": "sha512-NgVyR4hHHN2FvSNQOtbgBOuVsEdD/in30d9FKbEvvITiAChrBN2nBstmhfjI4EOTnHaP8zigwvkNYFI9yKGAkQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/firestore": "4.12.0", - "@firebase/firestore-types": "3.0.3", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/functions": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.2.tgz", - "integrity": "sha512-tHduUD+DeokM3NB1QbHCvEMoL16e8Z8JSkmuVA4ROoJKPxHn8ibnecHPO2e3nVCJR1D9OjuKvxz4gksfq92/ZQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check-interop-types": "0.3.3", - "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.7.1", - "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/functions-compat": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.2.tgz", - "integrity": "sha512-YNxgnezvZDkqxqXa6cT7/oTeD4WXbxgIP7qZp4LFnathQv5o2omM6EoIhXiT9Ie5AoQDcIhG9Y3/dj+DFJGaGQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/functions": "0.13.2", - "@firebase/functions-types": "0.6.3", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/installations": { - "version": "0.6.20", - "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.20.tgz", - "integrity": "sha512-LOzvR7XHPbhS0YB5ANXhqXB5qZlntPpwU/4KFwhSNpXNsGk/sBQ9g5hepi0y0/MfenJLe2v7t644iGOOElQaHQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/util": "1.14.0", - "idb": "7.1.1", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/installations-compat": { - "version": "0.2.20", - "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.20.tgz", - "integrity": "sha512-9C9pL/DIEGucmoPj8PlZTnztbX3nhNj5RTYVpUM7wQq/UlHywaYv99969JU/WHLvi9ptzIogXYS9d1eZ6XFe9g==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/installations": "0.6.20", - "@firebase/installations-types": "0.5.3", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@angular/fire/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/@angular/fire/node_modules/@firebase/messaging": { - "version": "0.12.24", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.24.tgz", - "integrity": "sha512-UtKoubegAhHyehcB7iQjvQ8OVITThPbbWk3g2/2ze42PrQr6oe6OmCElYQkBrE5RDCeMTNucXejbdulrQ2XwVg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/installations": "0.6.20", - "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.14.0", - "idb": "7.1.1", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/messaging-compat": { - "version": "0.2.24", - "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.24.tgz", - "integrity": "sha512-wXH8FrKbJvFuFe6v98TBhAtvgknxKIZtGM/wCVsfpOGmaAE80bD8tBxztl+uochjnFb9plihkd6mC4y7sZXSpA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/messaging": "0.12.24", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/performance": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.10.tgz", - "integrity": "sha512-8nRFld+Ntzp5cLKzZuG9g+kBaSn8Ks9dmn87UQGNFDygbmR6ebd8WawauEXiJjMj1n70ypkvAOdE+lzeyfXtGA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/installations": "0.6.20", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0", - "web-vitals": "^4.2.4" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/performance-compat": { - "version": "0.2.23", - "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.23.tgz", - "integrity": "sha512-c7qOAGBUAOpIuUlHu1axWcrCVtIYKPMhH0lMnoCDWnPwn1HcPuPUBVTWETbC7UWw71RMJF8DpirfWXzMWJQfgA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/logger": "0.5.0", - "@firebase/performance": "0.7.10", - "@firebase/performance-types": "0.2.3", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/remote-config": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.8.1.tgz", - "integrity": "sha512-L86TReBnPiiJOWd7k9iaiE9f7rHtMpjAoYN0fH2ey2ZRzsOChHV0s5sYf1+IIUYzplzsE46pjlmAUNkRRKwHSQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/installations": "0.6.20", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/remote-config-compat": { - "version": "0.2.22", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.22.tgz", - "integrity": "sha512-uW/eNKKtRBot2gnCC5mnoy5Voo2wMzZuQ7dwqqGHU176fO9zFgMwKiRzk+aaC99NLrFk1KOmr0ZVheD+zdJmjQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/logger": "0.5.0", - "@firebase/remote-config": "0.8.1", - "@firebase/remote-config-types": "0.5.0", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@angular/fire/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/@angular/fire/node_modules/@firebase/storage": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.1.tgz", - "integrity": "sha512-uIpYgBBsv1vIET+5xV20XT7wwqV+H4GFp6PBzfmLUcEgguS4SWNFof56Z3uOC2lNDh0KDda1UflYq2VwD9Nefw==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/storage-compat": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.1.tgz", - "integrity": "sha512-bgl3FHHfXAmBgzIK/Fps6Xyv2HiAQlSTov07CBL+RGGhrC5YIk4lruS8JVIC+UkujRdYvnf8cpQFGn2RCilJ/A==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/storage": "0.14.1", - "@firebase/storage-types": "0.8.3", - "@firebase/util": "1.14.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@angular/fire/node_modules/@firebase/util": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.14.0.tgz", - "integrity": "sha512-/gnejm7MKkVIXnSJGpc9L2CvvvzJvtDPeAEq5jAwgVlf/PeNxot+THx/bpD20wQ8uL5sz0xqgXy1nisOYMU+mw==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@angular/fire/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/@angular/fire/node_modules/firebase": { - "version": "12.10.0", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.10.0.tgz", - "integrity": "sha512-tAjHnEirksqWpa+NKDUSUMjulOnsTcsPC1X1rQ+gwPtjlhJS572na91CwaBXQJHXharIrfj7sw/okDkXOsphjA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/ai": "2.9.0", - "@firebase/analytics": "0.10.20", - "@firebase/analytics-compat": "0.2.26", - "@firebase/app": "0.14.9", - "@firebase/app-check": "0.11.1", - "@firebase/app-check-compat": "0.4.1", - "@firebase/app-compat": "0.5.9", - "@firebase/app-types": "0.9.3", - "@firebase/auth": "1.12.1", - "@firebase/auth-compat": "0.6.3", - "@firebase/data-connect": "0.4.0", - "@firebase/database": "1.1.1", - "@firebase/database-compat": "2.1.1", - "@firebase/firestore": "4.12.0", - "@firebase/firestore-compat": "0.4.6", - "@firebase/functions": "0.13.2", - "@firebase/functions-compat": "0.4.2", - "@firebase/installations": "0.6.20", - "@firebase/installations-compat": "0.2.20", - "@firebase/messaging": "0.12.24", - "@firebase/messaging-compat": "0.2.24", - "@firebase/performance": "0.7.10", - "@firebase/performance-compat": "0.2.23", - "@firebase/remote-config": "0.8.1", - "@firebase/remote-config-compat": "0.2.22", - "@firebase/storage": "0.14.1", - "@firebase/storage-compat": "0.4.1", - "@firebase/util": "1.14.0" - } - }, - "node_modules/@angular/forms": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.1.tgz", - "integrity": "sha512-6aqOPk9xoa0dfeUDeEbhaiPhmt6MQrdn59qbGAomn9RMXA925TrHbJhSIkp9tXc2Fr4aJRi8zkD/cdXEc1IYeA==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "tslib": "^2.3.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/common": "21.2.1", - "@angular/core": "21.2.1", - "@angular/platform-browser": "21.2.1", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/language-service": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-21.2.1.tgz", - "integrity": "sha512-L8EaNhWDKMny18RURg/Ju2Dix2e7qLL/s2yDQrawgjQRmXAMnjimz10w/EiiG7FMK/Hj5fLycS5X8VITq1f2rg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@angular/localize": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-21.2.1.tgz", - "integrity": "sha512-2QsN33fLO3N/RRFfUxDKHMX/Y/2TH90Tx51Wi6hi1do9IJdlfEe1qBw+5F0g1F1CuFEYgZWMJdZIK7LPHpuDzw==", - "license": "MIT", - "dependencies": { - "@babel/core": "7.29.0", - "@types/babel__core": "7.20.5", - "tinyglobby": "^0.2.12", - "yargs": "^18.0.0" - }, - "bin": { - "localize-extract": "tools/bundles/src/extract/cli.js", - "localize-migrate": "tools/bundles/src/migrate/cli.js", - "localize-translate": "tools/bundles/src/translate/cli.js" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/compiler": "21.2.1", - "@angular/compiler-cli": "21.2.1" - } - }, - "node_modules/@angular/platform-browser": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.1.tgz", - "integrity": "sha512-k4SJLxIaLT26vLjLuFL+ho0BiG5PrdxEsjsXFC7w5iUhomeouzkHVTZ4t7gaLNKrdRD7QNtU4Faw0nL0yx0ZPQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/animations": "21.2.1", - "@angular/common": "21.2.1", - "@angular/core": "21.2.1" - }, - "peerDependenciesMeta": { - "@angular/animations": { - "optional": true - } - } - }, - "node_modules/@angular/platform-browser-dynamic": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.2.1.tgz", - "integrity": "sha512-J4KnrXjgSuk7KjEm79/RK1yyzR867sIyT5mcG6jx2KmkjspFJd4OeOux7Oj7lSBM7+nDEsKC9F6s0x3dC0hCPQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/common": "21.2.1", - "@angular/compiler": "21.2.1", - "@angular/core": "21.2.1", - "@angular/platform-browser": "21.2.1" - } - }, - "node_modules/@angular/platform-server": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-21.2.1.tgz", - "integrity": "sha512-ezIY3hKl98JIjQcy+wq+38w9ln2bplQ8HFpEsaYySjPPu5TYCQ/z8HFxpvgqK80SVfvE7ijbgY+ALzKCE30Fjg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0", - "xhr2": "^0.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/common": "21.2.1", - "@angular/compiler": "21.2.1", - "@angular/core": "21.2.1", - "@angular/platform-browser": "21.2.1", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/router": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.1.tgz", - "integrity": "sha512-FUKG+8ImQYxmlDUdAs7+VeS/VrBNrbo0zGiKkzVNU/bbcCyroKXJLXFtkFI3qmROiJNyIta2IMBCHJvIjLIMig==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/common": "21.2.1", - "@angular/core": "21.2.1", - "@angular/platform-browser": "21.2.1", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@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/core/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==", - "license": "MIT" - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@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-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@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-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "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==", - "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==", - "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==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@cspotcode/source-map-consumer": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", - "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", - "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", - "license": "MIT", - "optional": true, - "dependencies": { - "@cspotcode/source-map-consumer": "0.8.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", - "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.3", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", - "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.1.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", - "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", - "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", - "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.1.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@firebase/ai": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-1.4.1.tgz", - "integrity": "sha512-bcusQfA/tHjUjBTnMx6jdoPMpDl3r8K15Z+snHz9wq0Foox0F/V+kNLXucEOHoTL2hTc9l+onZCyBJs2QoIC3g==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/app-check-interop-types": "0.3.3", - "@firebase/component": "0.6.18", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" - } - }, - "node_modules/@firebase/analytics": { - "version": "0.10.17", - "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.17.tgz", - "integrity": "sha512-n5vfBbvzduMou/2cqsnKrIes4auaBjdhg8QNA2ZQZ59QgtO2QiwBaXQZQE4O4sgB0Ds1tvLgUUkY+pwzu6/xEg==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/installations": "0.6.18", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/analytics-compat": { - "version": "0.2.23", - "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.23.tgz", - "integrity": "sha512-3AdO10RN18G5AzREPoFgYhW6vWXr3u+OYQv6pl3CX6Fky8QRk0AHurZlY3Q1xkXO0TDxIsdhO3y65HF7PBOJDw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/analytics": "0.10.17", - "@firebase/analytics-types": "0.8.3", - "@firebase/component": "0.6.18", - "@firebase/util": "1.12.1", - "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.13.2", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.2.tgz", - "integrity": "sha512-jwtMmJa1BXXDCiDx1vC6SFN/+HfYG53UkfJa6qeN5ogvOunzbFDO3wISZy5n9xgYFUrEP6M7e8EG++riHNTv9w==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.1", - "idb": "7.1.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@firebase/app-check": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.10.1.tgz", - "integrity": "sha512-MgNdlms9Qb0oSny87pwpjKush9qUwCJhfmTJHDfrcKo4neLGiSeVE4qJkzP7EQTIUFKp84pbTxobSAXkiuQVYQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/app-check-compat": { - "version": "0.3.26", - "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.26.tgz", - "integrity": "sha512-PkX+XJMLDea6nmnopzFKlr+s2LMQGqdyT2DHdbx1v1dPSqOol2YzgpgymmhC67vitXVpNvS3m/AiWQWWhhRRPQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/app-check": "0.10.1", - "@firebase/app-check-types": "0.5.3", - "@firebase/component": "0.6.18", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.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.4.2", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.4.2.tgz", - "integrity": "sha512-LssbyKHlwLeiV8GBATyOyjmHcMpX/tFjzRUCS1jnwGAew1VsBB4fJowyS5Ud5LdFbYpJeS+IQoC+RQxpK7eH3Q==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/app": "0.13.2", - "@firebase/component": "0.6.18", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.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" - }, - "node_modules/@firebase/auth": { - "version": "1.10.8", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.8.tgz", - "integrity": "sha512-GpuTz5ap8zumr/ocnPY57ZanX02COsXloY6Y/2LYPAuXYiaJRf6BAGDEdRq1BMjP93kqQnKNuKZUTMZbQ8MNYA==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x", - "@react-native-async-storage/async-storage": "^1.18.1" - }, - "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { - "optional": true - } - } - }, - "node_modules/@firebase/auth-compat": { - "version": "0.5.28", - "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.28.tgz", - "integrity": "sha512-HpMSo/cc6Y8IX7bkRIaPPqT//Jt83iWy5rmDWeThXQCAImstkdNo3giFLORJwrZw2ptiGkOij64EH1ztNJzc7Q==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/auth": "1.10.8", - "@firebase/auth-types": "0.13.0", - "@firebase/component": "0.6.18", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.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.6.18", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.18.tgz", - "integrity": "sha512-n28kPCkE2dL2U28fSxZJjzPPVpKsQminJ6NrzcKXAI0E/lYC8YhfwpyllScqVEvAI3J2QgJZWYgrX+1qGI+SQQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@firebase/data-connect": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.10.tgz", - "integrity": "sha512-VMVk7zxIkgwlVQIWHOKFahmleIjiVFwFOjmakXPd/LDgaB/5vzwsB5DWIYo+3KhGxWpidQlR8geCIn39YflJIQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.6.18", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/database": { - "version": "1.0.20", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.20.tgz", - "integrity": "sha512-H9Rpj1pQ1yc9+4HQOotFGLxqAXwOzCHsRSRjcQFNOr8lhUt6LeYjf0NSRL04sc4X0dWe8DsCvYKxMYvFG/iOJw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/app-check-interop-types": "0.3.3", - "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.6.18", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.1", - "faye-websocket": "0.11.4", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@firebase/database-compat": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.11.tgz", - "integrity": "sha512-itEsHARSsYS95+udF/TtIzNeQ0Uhx4uIna0sk4E0wQJBUnLc/G1X6D7oRljoOuwwCezRLGvWBRyNrugv/esOEw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/database": "1.0.20", - "@firebase/database-types": "1.0.15", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@firebase/database-types": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.15.tgz", - "integrity": "sha512-XWHJ0VUJ0k2E9HDMlKxlgy/ZuTa9EvHCGLjaKSUvrQnwhgZuRU5N3yX6SZ+ftf2hTzZmfRkv+b3QRvGg40bKNw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/app-types": "0.9.3", - "@firebase/util": "1.12.1" - } - }, - "node_modules/@firebase/firestore": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.8.0.tgz", - "integrity": "sha512-QSRk+Q1/CaabKyqn3C32KSFiOdZpSqI9rpLK5BHPcooElumOBooPFa6YkDdiT+/KhJtel36LdAacha9BptMj2A==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.1", - "@firebase/webchannel-wrapper": "1.0.3", - "@grpc/grpc-js": "~1.9.0", - "@grpc/proto-loader": "^0.7.8", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/firestore-compat": { - "version": "0.3.53", - "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.53.tgz", - "integrity": "sha512-qI3yZL8ljwAYWrTousWYbemay2YZa+udLWugjdjju2KODWtLG94DfO4NALJgPLv8CVGcDHNFXoyQexdRA0Cz8Q==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/firestore": "4.8.0", - "@firebase/firestore-types": "3.0.3", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.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.12.9", - "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.12.9.tgz", - "integrity": "sha512-FG95w6vjbUXN84Ehezc2SDjGmGq225UYbHrb/ptkRT7OTuCiQRErOQuyt1jI1tvcDekdNog+anIObihNFz79Lg==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/app-check-interop-types": "0.3.3", - "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.6.18", - "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/functions-compat": { - "version": "0.3.26", - "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.26.tgz", - "integrity": "sha512-A798/6ff5LcG2LTWqaGazbFYnjBW8zc65YfID/en83ALmkhu2b0G8ykvQnLtakbV9ajrMYPn7Yc/XcYsZIUsjA==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/functions": "0.12.9", - "@firebase/functions-types": "0.6.3", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.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.18", - "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.18.tgz", - "integrity": "sha512-NQ86uGAcvO8nBRwVltRL9QQ4Reidc/3whdAasgeWCPIcrhOKDuNpAALa6eCVryLnK14ua2DqekCOX5uC9XbU/A==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/util": "1.12.1", - "idb": "7.1.1", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/installations-compat": { - "version": "0.2.18", - "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.18.tgz", - "integrity": "sha512-aLFohRpJO5kKBL/XYL4tN+GdwEB/Q6Vo9eZOM/6Kic7asSUgmSfGPpGUZO1OAaSRGwF4Lqnvi1f/f9VZnKzChw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/installations": "0.6.18", - "@firebase/installations-types": "0.5.3", - "@firebase/util": "1.12.1", - "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.4.4", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.4.tgz", - "integrity": "sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@firebase/messaging": { - "version": "0.12.22", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.22.tgz", - "integrity": "sha512-GJcrPLc+Hu7nk+XQ70Okt3M1u1eRr2ZvpMbzbc54oTPJZySHcX9ccZGVFcsZbSZ6o1uqumm8Oc7OFkD3Rn1/og==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/installations": "0.6.18", - "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.12.1", - "idb": "7.1.1", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/messaging-compat": { - "version": "0.2.22", - "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.22.tgz", - "integrity": "sha512-5ZHtRnj6YO6f/QPa/KU6gryjmX4Kg33Kn4gRpNU6M1K47Gm8kcQwPkX7erRUYEH1mIWptfvjvXMHWoZaWjkU7A==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/messaging": "0.12.22", - "@firebase/util": "1.12.1", - "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.7", - "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.7.tgz", - "integrity": "sha512-JTlTQNZKAd4+Q5sodpw6CN+6NmwbY72av3Lb6wUKTsL7rb3cuBIhQSrslWbVz0SwK3x0ZNcqX24qtRbwKiv+6w==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/installations": "0.6.18", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0", - "web-vitals": "^4.2.4" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/performance-compat": { - "version": "0.2.20", - "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.20.tgz", - "integrity": "sha512-XkFK5NmOKCBuqOKWeRgBUFZZGz9SzdTZp4OqeUg+5nyjapTiZ4XoiiUL8z7mB2q+63rPmBl7msv682J3rcDXIQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/logger": "0.4.4", - "@firebase/performance": "0.7.7", - "@firebase/performance-types": "0.2.3", - "@firebase/util": "1.12.1", - "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.6.5", - "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.6.5.tgz", - "integrity": "sha512-fU0c8HY0vrVHwC+zQ/fpXSqHyDMuuuglV94VF6Yonhz8Fg2J+KOowPGANM0SZkLvVOYpTeWp3ZmM+F6NjwWLnw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/installations": "0.6.18", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/remote-config-compat": { - "version": "0.2.18", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.18.tgz", - "integrity": "sha512-YiETpldhDy7zUrnS8e+3l7cNs0sL7+tVAxvVYU0lu7O+qLHbmdtAxmgY+wJqWdW2c9nDvBFec7QiF58pEUu0qQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/logger": "0.4.4", - "@firebase/remote-config": "0.6.5", - "@firebase/remote-config-types": "0.4.0", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/remote-config-types": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz", - "integrity": "sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/@firebase/storage": { - "version": "0.13.14", - "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.13.14.tgz", - "integrity": "sha512-xTq5ixxORzx+bfqCpsh+o3fxOsGoDjC1nO0Mq2+KsOcny3l7beyBhP/y1u5T6mgsFQwI1j6oAkbT5cWdDBx87g==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/storage-compat": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.24.tgz", - "integrity": "sha512-XHn2tLniiP7BFKJaPZ0P8YQXKiVJX+bMyE2j2YWjYfaddqiJnROJYqSomwW6L3Y+gZAga35ONXUJQju6MB6SOQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/component": "0.6.18", - "@firebase/storage": "0.13.14", - "@firebase/storage-types": "0.8.3", - "@firebase/util": "1.12.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.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.12.1", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.12.1.tgz", - "integrity": "sha512-zGlBn/9Dnya5ta9bX/fgEoNC3Cp8s6h+uYPYaDieZsFOAdHP/ExzQ/eaDgxD3GOROdPkLKpvKY0iIzr9adle0w==", - "hasInstallScript": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@firebase/webchannel-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.3.tgz", - "integrity": "sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/@gar/promise-retry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.2.tgz", - "integrity": "sha512-Lm/ZLhDZcBECta3TmCQSngiQykFdfw+QtI1/GYMsZd4l3nG+P8WLB16XuS7WaBGLQ+9E+cOcWQsth9cayuGt8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "retry": "^0.13.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.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/@grpc/proto-loader/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/@grpc/proto-loader/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/@grpc/proto-loader/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/@grpc/proto-loader/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/@grpc/proto-loader/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/@grpc/proto-loader/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/@grpc/proto-loader/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/@grpc/proto-loader/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/@grpc/proto-loader/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/@harperfast/extended-iterable": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", - "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", - "dev": true, - "license": "Apache-2.0", - "optional": true - }, - "node_modules/@hono/node-server": { - "version": "1.19.11", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", - "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@inquirer/ansi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", - "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/checkbox": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", - "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/confirm": { - "version": "5.1.21", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", - "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "10.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", - "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/editor": { - "version": "4.2.23", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", - "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/external-editor": "^1.0.3", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/expand": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", - "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/external-editor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", - "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", - "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/input": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", - "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/number": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", - "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/password": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", - "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/prompts": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", - "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/checkbox": "^4.3.2", - "@inquirer/confirm": "^5.1.21", - "@inquirer/editor": "^4.2.23", - "@inquirer/expand": "^4.0.23", - "@inquirer/input": "^4.3.1", - "@inquirer/number": "^3.0.23", - "@inquirer/password": "^4.0.23", - "@inquirer/rawlist": "^4.1.11", - "@inquirer/search": "^3.2.2", - "@inquirer/select": "^4.4.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/rawlist": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", - "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/search": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", - "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/select": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", - "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/type": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", - "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jasminejs/reporters": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jasminejs/reporters/-/reporters-1.0.0.tgz", - "integrity": "sha512-rM3GG4vx2H1Gp5kYCTr9aKlOEJFd43pzpiMAiy5b1+FUc2ub4e6bS6yCi/WQNDzAa5MVp9++dwcoEtcIfoEnhA==", - "dev": true, - "license": "MIT" - }, - "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==", - "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==", - "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==", - "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==", - "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==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@listr2/prompt-adapter-inquirer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", - "integrity": "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/type": "^3.0.8" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@inquirer/prompts": ">= 3 < 8", - "listr2": "9.0.5" - } - }, - "node_modules/@lmdb/lmdb-darwin-arm64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.1.tgz", - "integrity": "sha512-tpfN4kKrrMpQ+If1l8bhmoNkECJi0iOu6AEdrTJvWVC+32sLxTARX5Rsu579mPImRP9YFWfWgeRQ5oav7zApQQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@lmdb/lmdb-darwin-x64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.1.tgz", - "integrity": "sha512-+a2tTfc3rmWhLAolFUWRgJtpSuu+Fw/yjn4rF406NMxhfjbMuiOUTDRvRlMFV+DzyjkwnokisskHbCWkS3Ly5w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@lmdb/lmdb-linux-arm": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.1.tgz", - "integrity": "sha512-0EgcE6reYr8InjD7V37EgXcYrloqpxVPINy3ig1MwDSbl6LF/vXTYRH9OE1Ti1D8YZnB35ZH9aTcdfSb5lql2A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@lmdb/lmdb-linux-arm64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.1.tgz", - "integrity": "sha512-aoERa5B6ywXdyFeYGQ1gbQpkMkDbEo45qVoXE5QpIRavqjnyPwjOulMkmkypkmsbJ5z4Wi0TBztON8agCTG0Vg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@lmdb/lmdb-linux-x64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.1.tgz", - "integrity": "sha512-SqNDY1+vpji7bh0sFH5wlWyFTOzjbDOl0/kB5RLLYDAFyd/uw3n7wyrmas3rYPpAW7z18lMOi1yKlTPv967E3g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@lmdb/lmdb-win32-arm64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.1.tgz", - "integrity": "sha512-50v0O1Lt37cwrmR9vWZK5hRW0Aw+KEmxJJ75fge/zIYdvNKB/0bSMSVR5Uc2OV9JhosIUyklOmrEvavwNJ8D6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@lmdb/lmdb-win32-x64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.1.tgz", - "integrity": "sha512-qwosvPyl+zpUlp3gRb7UcJ3H8S28XHCzkv0Y0EgQToXjQP91ZD67EHSCDmaLjtKhe+GVIW5om1KUpzVLA0l6pg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@napi-rs/nice": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", - "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "optionalDependencies": { - "@napi-rs/nice-android-arm-eabi": "1.1.1", - "@napi-rs/nice-android-arm64": "1.1.1", - "@napi-rs/nice-darwin-arm64": "1.1.1", - "@napi-rs/nice-darwin-x64": "1.1.1", - "@napi-rs/nice-freebsd-x64": "1.1.1", - "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", - "@napi-rs/nice-linux-arm64-gnu": "1.1.1", - "@napi-rs/nice-linux-arm64-musl": "1.1.1", - "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", - "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", - "@napi-rs/nice-linux-s390x-gnu": "1.1.1", - "@napi-rs/nice-linux-x64-gnu": "1.1.1", - "@napi-rs/nice-linux-x64-musl": "1.1.1", - "@napi-rs/nice-openharmony-arm64": "1.1.1", - "@napi-rs/nice-win32-arm64-msvc": "1.1.1", - "@napi-rs/nice-win32-ia32-msvc": "1.1.1", - "@napi-rs/nice-win32-x64-msvc": "1.1.1" - } - }, - "node_modules/@napi-rs/nice-android-arm-eabi": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", - "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-android-arm64": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", - "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-darwin-arm64": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", - "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-darwin-x64": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", - "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-freebsd-x64": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", - "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", - "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-arm64-gnu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", - "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-arm64-musl": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", - "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-ppc64-gnu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", - "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-riscv64-gnu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", - "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-s390x-gnu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", - "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-x64-gnu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", - "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-x64-musl": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", - "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-openharmony-arm64": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", - "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-win32-arm64-msvc": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", - "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-win32-ia32-msvc": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", - "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-win32-x64-msvc": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", - "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@npmcli/agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", - "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", - "dev": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^11.2.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@npmcli/fs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", - "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", - "dev": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/git": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", - "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", - "dev": true, - "license": "ISC", - "dependencies": { - "@gar/promise-retry": "^1.0.0", - "@npmcli/promise-spawn": "^9.0.0", - "ini": "^6.0.0", - "lru-cache": "^11.2.1", - "npm-pick-manifest": "^11.0.1", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/git/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@npmcli/git/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/installed-package-contents": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", - "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", - "dev": true, - "license": "ISC", - "dependencies": { - "npm-bundled": "^5.0.0", - "npm-normalize-package-bin": "^5.0.0" - }, - "bin": { - "installed-package-contents": "bin/index.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/node-gyp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", - "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/package-json": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", - "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^7.0.0", - "glob": "^13.0.0", - "hosted-git-info": "^9.0.0", - "json-parse-even-better-errors": "^5.0.0", - "proc-log": "^6.0.0", - "semver": "^7.5.3", - "spdx-expression-parse": "^4.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/promise-spawn": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", - "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/promise-spawn/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/redact": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", - "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/run-script": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", - "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/node-gyp": "^5.0.0", - "@npmcli/package-json": "^7.0.0", - "@npmcli/promise-spawn": "^9.0.0", - "node-gyp": "^12.1.0", - "proc-log": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.113.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", - "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", - "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.3", - "is-glob": "^4.0.3", - "node-addon-api": "^7.0.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.6", - "@parcel/watcher-darwin-arm64": "2.5.6", - "@parcel/watcher-darwin-x64": "2.5.6", - "@parcel/watcher-freebsd-x64": "2.5.6", - "@parcel/watcher-linux-arm-glibc": "2.5.6", - "@parcel/watcher-linux-arm-musl": "2.5.6", - "@parcel/watcher-linux-arm64-glibc": "2.5.6", - "@parcel/watcher-linux-arm64-musl": "2.5.6", - "@parcel/watcher-linux-x64-glibc": "2.5.6", - "@parcel/watcher-linux-x64-musl": "2.5.6", - "@parcel/watcher-win32-arm64": "2.5.6", - "@parcel/watcher-win32-ia32": "2.5.6", - "@parcel/watcher-win32-x64": "2.5.6" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", - "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", - "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", - "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", - "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", - "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", - "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", - "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", - "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", - "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", - "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", - "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", - "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", - "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "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/binding-android-arm64": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz", - "integrity": "sha512-vRq9f4NzvbdZavhQbjkJBx7rRebDKYR9zHfO/Wg486+I7bSecdUapzCm5cyXoK+LHokTxgSq7A5baAXUZkIz0w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.4.tgz", - "integrity": "sha512-kFgEvkWLqt3YCgKB5re9RlIrx9bRsvyVUnaTakEpOPuLGzLpLapYxE9BufJNvPg8GjT6mB1alN4yN1NjzoeM8Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.4.tgz", - "integrity": "sha512-JXmaOJGsL/+rsmMfutcDjxWM2fTaVgCHGoXS7nE8Z3c9NAYjGqHvXrAhMUZvMpHS/k7Mg+X7n/MVKb7NYWKKww==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.4.tgz", - "integrity": "sha512-ep3Catd6sPnHTM0P4hNEvIv5arnDvk01PfyJIJ+J3wVCG1eEaPo09tvFqdtcaTrkwQy0VWR24uz+cb4IsK53Qw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.4.tgz", - "integrity": "sha512-LwA5ayKIpnsgXJEwWc3h8wPiS33NMIHd9BhsV92T8VetVAbGe2qXlJwNVDGHN5cOQ22R9uYvbrQir2AB+ntT2w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.4.tgz", - "integrity": "sha512-AC1WsGdlV1MtGay/OQ4J9T7GRadVnpYRzTcygV1hKnypbYN20Yh4t6O1Sa2qRBMqv1etulUknqXjc3CTIsBu6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.4.tgz", - "integrity": "sha512-lU+6rgXXViO61B4EudxtVMXSOfiZONR29Sys5VGSetUY7X8mg9FCKIIjcPPj8xNDeYzKl+H8F/qSKOBVFJChCQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.4.tgz", - "integrity": "sha512-DZaN1f0PGp/bSvKhtw50pPsnln4T13ycDq1FrDWRiHmWt1JeW+UtYg9touPFf8yt993p8tS2QjybpzKNTxYEwg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.4.tgz", - "integrity": "sha512-RnGxwZLN7fhMMAItnD6dZ7lvy+TI7ba+2V54UF4dhaWa/p8I/ys1E73KO6HmPmgz92ZkfD8TXS1IMV8+uhbR9g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.4.tgz", - "integrity": "sha512-6lcI79+X8klGiGd8yHuTgQRjuuJYNggmEml+RsyN596P23l/zf9FVmJ7K0KVKkFAeYEdg0iMUKyIxiV5vebDNQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.4.tgz", - "integrity": "sha512-wz7ohsKCAIWy91blZ/1FlpPdqrsm1xpcEOQVveWoL6+aSPKL4VUcoYmmzuLTssyZxRpEwzuIxL/GDsvpjaBtOw==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.4.tgz", - "integrity": "sha512-cfiMrfuWCIgsFmcVG0IPuO6qTRHvF7NuG3wngX1RZzc6dU8FuBFb+J3MIR5WrdTNozlumfgL4cvz+R4ozBCvsQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.4.tgz", - "integrity": "sha512-p6UeR9y7ht82AH57qwGuFYn69S6CZ7LLKdCKy/8T3zS9VTrJei2/CGsTUV45Da4Z9Rbhc7G4gyWQ/Ioamqn09g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.4.tgz", - "integrity": "sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@schematics/angular": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.1.tgz", - "integrity": "sha512-DjrHRMoILhbZ6tc7aNZWuHA1wCm1iU/JN1TxAwNEyIBgyU3Fx8Z5baK4w0TCpOIPt0RLWVgP2L7kka9aXWCUFA==", - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "21.2.1", - "@angular-devkit/schematics": "21.2.1", - "jsonc-parser": "3.3.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@sigstore/bundle": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", - "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.5.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@sigstore/core": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.1.0.tgz", - "integrity": "sha512-o5cw1QYhNQ9IroioJxpzexmPjfCe7gzafd2RY3qnMpxr4ZEja+Jad/U8sgFpaue6bOaF+z7RVkyKVV44FN+N8A==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@sigstore/protobuf-specs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz", - "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@sigstore/sign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.0.tgz", - "integrity": "sha512-Vx1RmLxLGnSUqx/o5/VsCjkuN5L7y+vxEEwawvc7u+6WtX2W4GNa7b9HEjmcRWohw/d6BpATXmvOwc78m+Swdg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", - "@sigstore/protobuf-specs": "^0.5.0", - "make-fetch-happen": "^15.0.3", - "proc-log": "^6.1.0", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@sigstore/tuf": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.1.tgz", - "integrity": "sha512-OPZBg8y5Vc9yZjmWCHrlWPMBqW5yd8+wFNl+thMdtcWz3vjVSoJQutF8YkrzI0SLGnkuFof4HSsWUhXrf219Lw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.5.0", - "tuf-js": "^4.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@sigstore/verify": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz", - "integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", - "@sigstore/protobuf-specs": "^0.5.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "license": "MIT", - "optional": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "license": "MIT", - "optional": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "license": "MIT", - "optional": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "license": "MIT", - "optional": true - }, - "node_modules/@tufjs/canonical-json": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", - "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@tufjs/models": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", - "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tufjs/canonical-json": "2.0.0", - "minimatch": "^10.1.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "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==", - "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==", - "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==", - "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==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "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/jasmine": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-6.0.0.tgz", - "integrity": "sha512-18lgGsLmEh3VJk9eZ5wAjTISxdqzl6YOwu8UdMpolajN57QOCNbl+AbHUd+Yu9ItrsFdB+c8LSZSGNg8nHaguw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.19.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", - "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", - "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.1", - "@typescript-eslint/types": "^8.56.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", - "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", - "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", - "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", - "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@typescript-eslint/project-service": "8.56.1", - "@typescript-eslint/tsconfig-utils": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", - "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", - "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@typescript-eslint/types": "8.56.1", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@vitejs/plugin-basic-ssl": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", - "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "peerDependencies": { - "vite": "^6.0.0 || ^7.0.0" - } - }, - "node_modules/@yarnpkg/lockfile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/abbrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "devOptional": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "license": "MIT", - "optional": true, - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/algoliasearch": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.1.tgz", - "integrity": "sha512-Rf7xmeuIo7nb6S4mp4abW2faW8DauZyE2faBIKFaUfP3wnpOvNSbiI5AwVhqBNj0jPgBWEvhyCu0sLjN2q77Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/abtesting": "1.14.1", - "@algolia/client-abtesting": "5.48.1", - "@algolia/client-analytics": "5.48.1", - "@algolia/client-common": "5.48.1", - "@algolia/client-insights": "5.48.1", - "@algolia/client-personalization": "5.48.1", - "@algolia/client-query-suggestions": "5.48.1", - "@algolia/client-search": "5.48.1", - "@algolia/ingestion": "1.48.1", - "@algolia/monitoring": "1.48.1", - "@algolia/recommend": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/ansi-escapes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", - "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "license": "MIT", - "optional": true - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/beasties": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.1.tgz", - "integrity": "sha512-2Imdcw3LznDuxAbJM26RHniOLAzE6WgrK8OuvVXCQtNBS8rsnD9zsSEa3fHl4hHpUY7BYTlrpvtPVbvu9G6neg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "css-select": "^6.0.0", - "css-what": "^7.0.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "htmlparser2": "^10.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.49", - "postcss-media-query-parser": "^0.2.3", - "postcss-safe-parser": "^7.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" - }, - "node_modules/bootstrap": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", - "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "license": "MIT", - "peerDependencies": { - "@popperjs/core": "^2.11.8" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "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": { - "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/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cacache": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", - "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^5.0.0", - "fs-minipass": "^3.0.0", - "glob": "^13.0.0", - "lru-cache": "^11.1.0", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^13.0.0", - "unique-filename": "^5.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001777", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", - "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", - "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/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chardet": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", - "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", - "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", - "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^8.0.0", - "string-width": "^8.2.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", - "license": "ISC", - "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/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/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/connect": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/connect/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/connect/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/connect/node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/connect/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/connect/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/connect/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/core-js": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", - "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "license": "MIT", - "optional": true - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-select": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", - "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^7.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "nth-check": "^2.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", - "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/custom-event": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", - "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", - "dev": true, - "license": "MIT" - }, - "node_modules/date-format": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", - "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/di": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", - "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", - "dev": true, - "license": "MIT" - }, - "node_modules/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dom-serialize": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", - "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "custom-event": "~1.0.0", - "ent": "~2.2.0", - "extend": "^3.0.0", - "void-elements": "^2.0.0" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.307", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", - "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", - "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/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/engine.io": { - "version": "6.6.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", - "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.4.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.18.3" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ent": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", - "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "punycode": "^1.4.1", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "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/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", - "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.3", - "@eslint/config-helpers": "^0.5.2", - "@eslint/core": "^1.1.1", - "@eslint/plugin-kit": "^0.6.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.1.1", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true, - "license": "MIT" - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", - "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "10.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/firebase": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-11.10.0.tgz", - "integrity": "sha512-nKBXoDzF0DrXTBQJlZa+sbC5By99ysYU1D6PkMRYknm0nCW7rJly47q492Ht7Ndz5MeYSBuboKuhS1e6mFC03w==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@firebase/ai": "1.4.1", - "@firebase/analytics": "0.10.17", - "@firebase/analytics-compat": "0.2.23", - "@firebase/app": "0.13.2", - "@firebase/app-check": "0.10.1", - "@firebase/app-check-compat": "0.3.26", - "@firebase/app-compat": "0.4.2", - "@firebase/app-types": "0.9.3", - "@firebase/auth": "1.10.8", - "@firebase/auth-compat": "0.5.28", - "@firebase/data-connect": "0.3.10", - "@firebase/database": "1.0.20", - "@firebase/database-compat": "2.0.11", - "@firebase/firestore": "4.8.0", - "@firebase/firestore-compat": "0.3.53", - "@firebase/functions": "0.12.9", - "@firebase/functions-compat": "0.3.26", - "@firebase/installations": "0.6.18", - "@firebase/installations-compat": "0.2.18", - "@firebase/messaging": "0.12.22", - "@firebase/messaging-compat": "0.2.22", - "@firebase/performance": "0.7.7", - "@firebase/performance-compat": "0.2.20", - "@firebase/remote-config": "0.6.5", - "@firebase/remote-config-compat": "0.2.18", - "@firebase/storage": "0.13.14", - "@firebase/storage-compat": "0.3.24", - "@firebase/util": "1.12.1" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", - "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", - "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/font-awesome": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", - "integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==", - "license": "(OFL-1.1 AND MIT)", - "engines": { - "node": ">=0.10.3" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "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/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/fuzzy": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", - "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==", - "dev": true, - "engines": { - "node": ">= 0.6.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==", - "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/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.12.5", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", - "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/hosted-git-info": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", - "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^11.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/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/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/ignore-walk": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", - "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minimatch": "^10.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/immutable": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", - "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", - "dev": true, - "license": "MIT" - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ini": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", - "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jasmine": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-6.1.0.tgz", - "integrity": "sha512-WPphPqEMY0uBRMjuhRHoVoxQNvJuxIMqz0yIcJ3k3oYxBedeGoH60/NXNgasxnx2FvfXrq5/r+2wssJ7WE8ABw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jasminejs/reporters": "^1.0.0", - "glob": "^10.2.2 || ^11.0.3 || ^12.0.0 || ^13.0.0", - "jasmine-core": "~6.1.0" - }, - "bin": { - "jasmine": "bin/jasmine.js" - } - }, - "node_modules/jasmine-core": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-6.1.0.tgz", - "integrity": "sha512-p/tjBw58O6vxKIWMlrU+yys8lqR3+l3UrqwNTT7wpj+dQ7N4etQekFM8joI+cWzPDYqZf54kN+hLC1+s5TvZvg==", - "dev": true, - "license": "MIT" - }, - "node_modules/jasmine-spec-reporter": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-7.0.0.tgz", - "integrity": "sha512-OtC7JRasiTcjsaCBPtMO0Tl8glCejM4J4/dNuOJdA8lBjz4PmWjYQ6pzb0uzpBNAWJMDudYuj9OdXJWqM2QTJg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "colors": "1.4.0" - } - }, - "node_modules/jose": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.0.tgz", - "integrity": "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", - "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", - "dev": true, - "engines": [ - "node >= 0.2.0" - ], - "license": "MIT" - }, - "node_modules/karma": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", - "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@colors/colors": "1.5.0", - "body-parser": "^1.19.0", - "braces": "^3.0.2", - "chokidar": "^3.5.1", - "connect": "^3.7.0", - "di": "^0.0.1", - "dom-serialize": "^2.2.1", - "glob": "^7.1.7", - "graceful-fs": "^4.2.6", - "http-proxy": "^1.18.1", - "isbinaryfile": "^4.0.8", - "lodash": "^4.17.21", - "log4js": "^6.4.1", - "mime": "^2.5.2", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.5", - "qjobs": "^1.2.0", - "range-parser": "^1.2.1", - "rimraf": "^3.0.2", - "socket.io": "^4.7.2", - "source-map": "^0.6.1", - "tmp": "^0.2.1", - "ua-parser-js": "^0.7.30", - "yargs": "^16.1.1" - }, - "bin": { - "karma": "bin/karma" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/karma-chrome-launcher": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", - "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "which": "^1.2.1" - } - }, - "node_modules/karma-chrome-launcher/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/karma-coverage": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", - "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.1", - "istanbul-reports": "^3.0.5", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/karma-coverage/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/karma-coverage/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/karma-coverage/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/karma-coverage/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/karma-jasmine": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", - "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "jasmine-core": "^4.1.0" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "karma": "^6.0.0" - } - }, - "node_modules/karma-jasmine-html-reporter": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.2.0.tgz", - "integrity": "sha512-J0laEC43Oy2RdR5V5R3bqmdo7yRIYySq6XHKbA+e5iSAgLjhR1oICLGeSREPlJXpeyNcdJf3J17YcdhD0mRssQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "jasmine-core": "^4.0.0 || ^5.0.0 || ^6.0.0", - "karma": "^6.0.0", - "karma-jasmine": "^5.0.0" - } - }, - "node_modules/karma-jasmine/node_modules/jasmine-core": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", - "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/karma-junit-reporter": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/karma-junit-reporter/-/karma-junit-reporter-2.0.1.tgz", - "integrity": "sha512-VtcGfE0JE4OE1wn0LK8xxDKaTP7slN8DO3I+4xg6gAi1IoAHAXOJ1V9G/y45Xg6sxdxPOR3THCFtDlAfBo9Afw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-is-absolute": "^1.0.0", - "xmlbuilder": "12.0.0" - }, - "engines": { - "node": ">= 8" - }, - "peerDependencies": { - "karma": ">=0.9" - } - }, - "node_modules/karma/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/karma/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/karma/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/karma/node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/karma/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/karma/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/karma/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/karma/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/karma/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/karma/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/karma/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/karma/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/karma/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/karma/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/karma/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/karma/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/karma/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/karma/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/karma/node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/karma/node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/karma/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/karma/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/karma/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "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/karma/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/karma/node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/karma/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==", - "dev": true, - "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/karma/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/karma/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/listr2": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", - "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/lmdb": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.1.tgz", - "integrity": "sha512-NYHA0MRPjvNX+vSw8Xxg6FLKxzAG+e7Pt8RqAQA/EehzHVXq9SxDqJIN3JL1hK0dweb884y8kIh6rkWvPyg9Wg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@harperfast/extended-iterable": "^1.0.3", - "msgpackr": "^1.11.2", - "node-addon-api": "^6.1.0", - "node-gyp-build-optional-packages": "5.2.2", - "ordered-binary": "^1.5.3", - "weak-lru-cache": "^1.2.2" - }, - "bin": { - "download-lmdb-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@lmdb/lmdb-darwin-arm64": "3.5.1", - "@lmdb/lmdb-darwin-x64": "3.5.1", - "@lmdb/lmdb-linux-arm": "3.5.1", - "@lmdb/lmdb-linux-arm64": "3.5.1", - "@lmdb/lmdb-linux-x64": "3.5.1", - "@lmdb/lmdb-win32-arm64": "3.5.1", - "@lmdb/lmdb-win32-x64": "3.5.1" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, - "license": "MIT" - }, - "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/log-symbols": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", - "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0", - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/log4js": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", - "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "flatted": "^3.2.7", - "rfdc": "^1.3.0", - "streamroller": "^3.1.5" - }, - "engines": { - "node": ">=8.0" - } - }, - "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==", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "license": "ISC", - "optional": true - }, - "node_modules/make-fetch-happen": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.4.tgz", - "integrity": "sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@gar/promise-retry": "^1.0.0", - "@npmcli/agent": "^4.0.0", - "cacache": "^20.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^5.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^6.0.0", - "ssri": "^13.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-fetch": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", - "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^2.0.0", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - }, - "optionalDependencies": { - "iconv-lite": "^0.7.2" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-sized": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", - "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/msgpackr": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.8.tgz", - "integrity": "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==", - "dev": true, - "license": "MIT", - "optional": true, - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" - } - }, - "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "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/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-gyp": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", - "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^15.0.0", - "nopt": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "tar": "^7.5.4", - "tinyglobby": "^0.2.12", - "which": "^6.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/node-gyp/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", - "license": "MIT" - }, - "node_modules/nopt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-bundled": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", - "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", - "dev": true, - "license": "ISC", - "dependencies": { - "npm-normalize-package-bin": "^5.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-install-checks": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", - "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "semver": "^7.1.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-normalize-package-bin": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", - "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-package-arg": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", - "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", - "dev": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^7.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-packlist": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", - "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", - "dev": true, - "license": "ISC", - "dependencies": { - "ignore-walk": "^8.0.0", - "proc-log": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-pick-manifest": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", - "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "npm-install-checks": "^8.0.0", - "npm-normalize-package-bin": "^5.0.0", - "npm-package-arg": "^13.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-registry-fetch": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", - "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/redact": "^4.0.0", - "jsonparse": "^1.3.1", - "make-fetch-happen": "^15.0.0", - "minipass": "^7.0.2", - "minipass-fetch": "^5.0.0", - "minizlib": "^3.0.1", - "npm-package-arg": "^13.0.0", - "proc-log": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ora": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", - "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", - "license": "MIT", - "dependencies": { - "chalk": "^5.6.2", - "cli-cursor": "^5.0.0", - "cli-spinners": "^3.2.0", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.1.0", - "log-symbols": "^7.0.1", - "stdin-discarder": "^0.3.1", - "string-width": "^8.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ordered-binary": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", - "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pacote": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.3.1.tgz", - "integrity": "sha512-O0EDXi85LF4AzdjG74GUwEArhdvawi/YOHcsW6IijKNj7wm8IvEWNF5GnfuxNpQ/ZpO3L37+v8hqdVh8GgWYhg==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^7.0.0", - "@npmcli/installed-package-contents": "^4.0.0", - "@npmcli/package-json": "^7.0.0", - "@npmcli/promise-spawn": "^9.0.0", - "@npmcli/run-script": "^10.0.0", - "cacache": "^20.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^7.0.2", - "npm-package-arg": "^13.0.0", - "npm-packlist": "^10.0.1", - "npm-pick-manifest": "^11.0.1", - "npm-registry-fetch": "^19.0.0", - "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", - "sigstore": "^4.0.0", - "ssri": "^13.0.0", - "tar": "^7.4.3" - }, - "bin": { - "pacote": "bin/index.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-html-rewriting-stream": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.0.tgz", - "integrity": "sha512-wzh11mj8KKkno1pZEu+l2EVeWsuKDfR5KNWZOTsslfUX8lPDZx77m9T0kIoAVkFtD1nx6YF8oh4BnPHvxMtNMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0", - "parse5": "^8.0.0", - "parse5-sax-parser": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-html-rewriting-stream/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/parse5-sax-parser": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", - "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse5": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/piscina": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz", - "integrity": "sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.x" - }, - "optionalDependencies": { - "@napi-rs/nice": "^1.0.4" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "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/postcss-media-query-parser": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", - "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-safe-parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", - "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/promise-retry/node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "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/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/qjobs": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", - "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.9" - } - }, - "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0" - }, - "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/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/rolldown": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.4.tgz", - "integrity": "sha512-V2tPDUrY3WSevrvU2E41ijZlpF+5PbZu4giH+VpNraaadsJGHa4fR6IFwsocVwEXDoAdIv5qgPPxgrvKAOIPtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.113.0", - "@rolldown/pluginutils": "1.0.0-rc.4" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.4", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.4", - "@rolldown/binding-darwin-x64": "1.0.0-rc.4", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.4", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.4", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.4", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.4", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.4", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.4", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.4", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.4", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.4", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.4" - } - }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "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.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/rxfire": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/rxfire/-/rxfire-6.1.0.tgz", - "integrity": "sha512-NezdjeY32VZcCuGO0bbb8H8seBsJSCaWdUwGsHNzUcAOHR0VGpzgPtzjuuLXr8R/iemkqSzbx/ioS7VwV43ynA==", - "license": "Apache-2.0", - "peerDependencies": { - "firebase": "^9.0.0 || ^10.0.0 || ^11.0.0", - "rxjs": "^6.0.0 || ^7.0.0" - } - }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "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/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sass": { - "version": "1.97.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", - "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, - "node_modules/sass/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/sass/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sigstore": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz", - "integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", - "@sigstore/protobuf-specs": "^0.5.0", - "@sigstore/sign": "^4.1.0", - "@sigstore/tuf": "^4.0.1", - "@sigstore/verify": "^3.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/slice-ansi": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", - "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "is-fullwidth-code-point": "^5.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socket.io": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", - "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.4.1", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", - "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "~4.4.1", - "ws": "~8.18.3" - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", - "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "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/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", - "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", - "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/ssri": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", - "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stdin-discarder": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz", - "integrity": "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/streamroller": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", - "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "fs-extra": "^8.1.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/string-width": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", - "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", - "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "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/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-node": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", - "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", - "license": "MIT", - "optional": true, - "dependencies": { - "@cspotcode/source-map-support": "0.7.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.0", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tuf-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", - "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tufjs/models": "4.1.0", - "debug": "^4.4.3", - "make-fetch-happen": "^15.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ua-parser-js": { - "version": "0.7.41", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz", - "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "MIT", - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/undici": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", - "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/unique-filename": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz", - "integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/unique-slug": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz", - "integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "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==", - "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/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/uri-js/node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "license": "MIT", - "optional": true - }, - "node_modules/validate-npm-package-name": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", - "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "dev": true, - "license": "MIT", - "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/void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/weak-lru-cache": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", - "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", - "dev": true, - "license": "MIT", - "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/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "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/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xhr2": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", - "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/xmlbuilder": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-12.0.0.tgz", - "integrity": "sha512-lMo8DJ8u6JRWp0/Y4XLa/atVDr75H9litKlb2E5j3V3MesoL50EBgZDWoLT3F/LztVnG67GjPXLZpqcky/UMnQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "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==", - "license": "ISC" - }, - "node_modules/yargs": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", - "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", - "license": "MIT", - "dependencies": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^7.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "license": "ISC", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yoctocolors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", - "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.25 || ^4" - } - }, - "node_modules/zone.js": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.1.tgz", - "integrity": "sha512-dpvY17vxYIW3+bNrP0ClUlaiY0CiIRK3tnoLaGoQsQcY9/I/NpzIWQ7tQNhbV7LacQMpCII6wVzuL3tuWOyfuA==", - "license": "MIT" - } - } -} diff --git a/samples/angular/package.json b/samples/angular/package.json deleted file mode 100644 index cbb28ecc..00000000 --- a/samples/angular/package.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "name": "keeptrack-angularwebapp", - "version": "0.1.0", - "scripts": { - "ng": "ng", - "start": "ng serve --configuration=dev", - "build": "ng build", - "watch": "ng build --watch --configuration dev", - "test": "ng test --configuration dev", - "test:ci": "ng test --no-watch --configuration=dev --code-coverage --browsers ChromeHeadless", - "lint": "ng lint", - "e2e": "ng e2e" - }, - "private": true, - "dependencies": { - "@angular/animations": "^21.2.1", - "@angular/common": "^21.2.1", - "@angular/compiler": "^21.2.1", - "@angular/core": "^21.2.1", - "@angular/fire": "^21.0.0-rc.0", - "@angular/forms": "^21.2.1", - "@angular/localize": "^21.2.1", - "@angular/platform-browser": "^21.2.1", - "@angular/platform-browser-dynamic": "^21.2.1", - "@angular/platform-server": "^21.2.1", - "@angular/router": "^21.2.1", - "@popperjs/core": "~2.11.8", - "bootstrap": "~5.3.2", - "core-js": "^3.48.0", - "font-awesome": "^4.7.0", - "rxjs": "~7.8.2", - "tslib": "^2.8.1", - "zone.js": "~0.16.1" - }, - "devDependencies": { - "@angular-devkit/architect": "^0.2102.1", - "@angular-eslint/builder": "^21.3.0", - "@angular-eslint/eslint-plugin": "^21.3.0", - "@angular-eslint/eslint-plugin-template": "^21.3.0", - "@angular-eslint/schematics": "^21.3.0", - "@angular-eslint/template-parser": "^21.3.0", - "@angular/build": "^21.2.1", - "@angular/cli": "^21.2.1", - "@angular/compiler-cli": "^21.2.1", - "@angular/language-service": "^21.2.1", - "@types/jasmine": "~6.0.0", - "@types/node": "^22.13.14", - "acorn": "~8.16.0", - "eslint": "^10.0.3", - "fuzzy": "~0.1.3", - "glob": "^13.0.6", - "jasmine": "~6.1.0", - "jasmine-core": "~6.1.0", - "jasmine-spec-reporter": "~7.0.0", - "karma": "~6.4.4", - "karma-chrome-launcher": "~3.2.0", - "karma-coverage": "~2.2.1", - "karma-jasmine": "~5.1.0", - "karma-jasmine-html-reporter": "~2.2.0", - "karma-junit-reporter": "~2.0.1", - "typescript": "~5.9.3" - }, - "optionalDependencies": { - "ts-node": "~10.7.0" - } -} diff --git a/samples/angular/src/app/app.component.html b/samples/angular/src/app/app.component.html deleted file mode 100644 index 8a51c496..00000000 --- a/samples/angular/src/app/app.component.html +++ /dev/null @@ -1,6 +0,0 @@ - - -
- -
- diff --git a/samples/angular/src/app/app.component.ts b/samples/angular/src/app/app.component.ts deleted file mode 100644 index dcbdb596..00000000 --- a/samples/angular/src/app/app.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; -import { HeaderComponent } from './layout/header/header.component'; - -@Component({ - selector: 'app-root', - imports: [ - RouterOutlet, - HeaderComponent - ], - templateUrl: './app.component.html' -}) -export class AppComponent { - title = 'app'; -} diff --git a/samples/angular/src/app/app.routes.ts b/samples/angular/src/app/app.routes.ts deleted file mode 100644 index 438e2070..00000000 --- a/samples/angular/src/app/app.routes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Routes } from '@angular/router'; -import { AuthGuard } from '@angular/fire/auth-guard'; - -import { HomeComponent } from './home/home.component'; -import { BookComponent } from './inventory/book/book.component'; -import { CarComponent } from './inventory/car/car.component'; -import { MovieComponent } from './inventory/movie/movie.component'; -import { TvShowComponent } from './inventory/tv-show/tv-show.component'; -import { VideoGameComponent } from './inventory/video-game/video-game.component'; -import { LoginComponent } from './user/login/login.component'; - -export const routes: Routes = [ - { path: '', component: HomeComponent, pathMatch: 'full' }, - { path: 'login', component: LoginComponent }, - { path: 'movies', component: MovieComponent, canActivate: [AuthGuard] }, - { path: 'books', component: BookComponent, canActivate: [AuthGuard] }, - { path: 'cars', component: CarComponent, canActivate: [AuthGuard] }, - { path: 'tv-shows', component: TvShowComponent, canActivate: [AuthGuard] }, - { path: 'video-games', component: VideoGameComponent, canActivate: [AuthGuard] } -]; diff --git a/samples/angular/src/app/backend/backend.module.ts b/samples/angular/src/app/backend/backend.module.ts deleted file mode 100644 index caa787d1..00000000 --- a/samples/angular/src/app/backend/backend.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { MovieService } from './services/movie.service'; -import { CarHistoryService } from './services/car-history.service'; - -@NgModule({ - declarations: [], - imports: [ - CommonModule - ], - providers: [ - CarHistoryService, - MovieService - ] -}) -export class BackendModule { } diff --git a/samples/angular/src/app/backend/services/book.service.spec.ts b/samples/angular/src/app/backend/services/book.service.spec.ts deleted file mode 100644 index 1312587e..00000000 --- a/samples/angular/src/app/backend/services/book.service.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; - -import { BookService } from './book.service'; -import { Book } from '../types/book'; -import { environment } from 'src/environments/environment.dev'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; - -describe('BookService', () => { - let bookService: BookService; - let http: HttpTestingController; - - beforeEach(() => TestBed.configureTestingModule({ - imports: [], - providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] -})); - - beforeEach(() => { - http = TestBed.inject(HttpTestingController); - bookService = TestBed.inject(BookService); - }); - - afterAll(() => http.verify()); - - it('should list', () => { - // fake response - const hardcodedBooks = [{ title: 'The fellowship of the Ring' }, { title: 'The two Towers' }] as Array; - - let actualBooks: Array = []; - bookService.list().subscribe((books: Array) => actualBooks = books); - - http.expectOne(`${environment.keepTrackApiUrl}/api/books?search=&page=0&pageSize=50`) - .flush(hardcodedBooks); - - expect(actualBooks).toEqual(hardcodedBooks, 'The `list` method should return an array of Book wrapped in an Observable'); - }); -}); diff --git a/samples/angular/src/app/backend/services/book.service.ts b/samples/angular/src/app/backend/services/book.service.ts deleted file mode 100644 index e4ee8478..00000000 --- a/samples/angular/src/app/backend/services/book.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { environment } from 'src/environments/environment'; -import { Book } from '../types/book'; -import { DataService } from './data.interface'; - -@Injectable({ - providedIn: 'root' -}) -export class BookService implements DataService { - private httpClient = inject(HttpClient); - - get(id: string): Observable { - return this.httpClient.get(`${environment.keepTrackApiUrl}/api/books/${id}`); - } - - list(search?: string, currentPage?: number, pageSize?: number): Observable> { - return this.httpClient.get>(`${environment.keepTrackApiUrl}/api/books?search=${search ?? ''}&page=${currentPage ?? 0}&pageSize=${pageSize ?? 50}`); - } - - create(input: Book): Observable { - return this.httpClient.post(`${environment.keepTrackApiUrl}/api/books`, input); - } - - update(input: Book): Observable { - delete input.isEditable; - if (!input.finishedAt) { - delete input.finishedAt; - } - return this.httpClient.put(`${environment.keepTrackApiUrl}/api/books/${input.id}`, input); - } - - delete(input: Book): Observable { - return this.httpClient.delete(`${environment.keepTrackApiUrl}/api/books/${input.id}`); - } -} diff --git a/samples/angular/src/app/backend/services/car-history.service.spec.ts b/samples/angular/src/app/backend/services/car-history.service.spec.ts deleted file mode 100644 index 1f1e53ed..00000000 --- a/samples/angular/src/app/backend/services/car-history.service.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { CarHistoryService } from './car-history.service'; - -describe('CarHistoryService', () => { - beforeEach(() => TestBed.configureTestingModule({})); - - it('should be created', () => { - const service: CarHistoryService = TestBed.inject(CarHistoryService); - expect(service).toBeTruthy(); - }); -}); diff --git a/samples/angular/src/app/backend/services/car-history.service.ts b/samples/angular/src/app/backend/services/car-history.service.ts deleted file mode 100644 index efb1657c..00000000 --- a/samples/angular/src/app/backend/services/car-history.service.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root' -}) -export class CarHistoryService { -} diff --git a/samples/angular/src/app/backend/services/data.interface.ts b/samples/angular/src/app/backend/services/data.interface.ts deleted file mode 100644 index 399a0589..00000000 --- a/samples/angular/src/app/backend/services/data.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Observable } from "rxjs"; - -export interface DataService { - get(id: string): Observable; - list(search?: string, currentPage?: number, pageSize?: number, filter?: T): Observable>; - create(input: T): Observable; - update(input: T): Observable; - delete(input: T): Observable; -} diff --git a/samples/angular/src/app/backend/services/movie.service.spec.ts b/samples/angular/src/app/backend/services/movie.service.spec.ts deleted file mode 100644 index 73e63c11..00000000 --- a/samples/angular/src/app/backend/services/movie.service.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; - -import { environment } from 'src/environments/environment.dev'; -import { MovieService } from './movie.service'; -import { Movie } from '../types/movie'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; - -describe('MovieService', () => { - let movieService: MovieService; - let http: HttpTestingController; - - beforeEach(() => TestBed.configureTestingModule({ - imports: [], - providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] -})); - - beforeEach(() => { - http = TestBed.inject(HttpTestingController); - movieService = TestBed.inject(MovieService); - }); - - afterAll(() => http.verify()); - - it('should list', () => { - // fake response - const hardcodedMovies = [{ title: 'Terminator 1' }, { title: 'Terminator 2' }] as Array; - - let actualMovies: Array = []; - movieService.list().subscribe((movies: Array) => actualMovies = movies); - - http.expectOne(`${environment.keepTrackApiUrl}/api/movies?search=&page=0&pageSize=50`) - .flush(hardcodedMovies); - - expect(actualMovies).toEqual(hardcodedMovies, 'The `list` method should return an array of Movie wrapped in an Observable'); - }); -}); diff --git a/samples/angular/src/app/backend/services/movie.service.ts b/samples/angular/src/app/backend/services/movie.service.ts deleted file mode 100644 index e5d07315..00000000 --- a/samples/angular/src/app/backend/services/movie.service.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { environment } from 'src/environments/environment'; -import { DataService } from './data.interface'; -import { Movie } from '../types/movie'; - -@Injectable({ - providedIn: 'root' -}) -export class MovieService implements DataService { - private httpClient = inject(HttpClient); - - get(id: string): Observable { - return this.httpClient.get(`${environment.keepTrackApiUrl}/api/movies/${id}`); - } - - list(search?: string, currentPage?: number, pageSize?: number): Observable> { - return this.httpClient.get>(`${environment.keepTrackApiUrl}/api/movies?search=${search ?? ''}&page=${currentPage ?? 0}&pageSize=${pageSize ?? 50}`); - } - - create(input: Movie): Observable { - return this.httpClient.post(`${environment.keepTrackApiUrl}/api/movies`, input); - } - - update(input: Movie): Observable { - delete input.isEditable; - if (!input.year) { - delete input.year; - } - - return this.httpClient.put(`${environment.keepTrackApiUrl}/api/movies/${input.id}`, input); - } - - delete(input: Movie): Observable { - return this.httpClient.delete(`${environment.keepTrackApiUrl}/api/movies/${input.id}`); - } -} diff --git a/samples/angular/src/app/backend/services/tv-show.service.spec.ts b/samples/angular/src/app/backend/services/tv-show.service.spec.ts deleted file mode 100644 index ccdec45c..00000000 --- a/samples/angular/src/app/backend/services/tv-show.service.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; -import { environment } from 'src/environments/environment.dev'; -import { TvShow } from '../types/tv-show'; -import { TvShowService } from './tv-show.service'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; - -describe('TvShowService', () => { - let service: TvShowService; - let http: HttpTestingController; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [], - providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] -}); - http = TestBed.inject(HttpTestingController); - service = TestBed.inject(TvShowService); - }); - - afterAll(() => http.verify()); - - it('should list', () => { - // fake response - const fake = [{ title: 'Friends' }, { title: 'ER' }] as Array; - - let actual: Array = []; - service.list().subscribe((movies: Array) => actual = movies); - - http.expectOne(`${environment.keepTrackApiUrl}/api/tv-shows?search=&page=0&pageSize=50`) - .flush(fake); - - expect(actual).toEqual(fake, 'The `list` method should return an array of TV Shows wrapped in an Observable'); - }); - -}); diff --git a/samples/angular/src/app/backend/services/tv-show.service.ts b/samples/angular/src/app/backend/services/tv-show.service.ts deleted file mode 100644 index d731edb2..00000000 --- a/samples/angular/src/app/backend/services/tv-show.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; -import { Observable } from 'rxjs'; -import { environment } from 'src/environments/environment'; -import { TvShow } from '../types/tv-show'; -import { DataService } from './data.interface'; - -@Injectable({ - providedIn: 'root' -}) -export class TvShowService implements DataService { - private httpClient = inject(HttpClient); - - get(id: string): Observable { - return this.httpClient.get(`${environment.keepTrackApiUrl}/api/tv-shows/${id}`); - } - - list(search?: string, currentPage?: number, pageSize?: number): Observable> { - return this.httpClient.get>(`${environment.keepTrackApiUrl}/api/tv-shows?search=${search ?? ''}&page=${currentPage ?? 0}&pageSize=${pageSize ?? 50}`); - } - - create(input: TvShow): Observable { - return this.httpClient.post(`${environment.keepTrackApiUrl}/api/tv-shows`, input); - } - - update(input: TvShow): Observable { - delete input.isEditable; - return this.httpClient.put(`${environment.keepTrackApiUrl}/api/tv-shows/${input.id}`, input); - } - - delete(input: TvShow): Observable { - return this.httpClient.delete(`${environment.keepTrackApiUrl}/api/tv-shows/${input.id}`); - } -} diff --git a/samples/angular/src/app/backend/services/video-game.service.spec.ts b/samples/angular/src/app/backend/services/video-game.service.spec.ts deleted file mode 100644 index 7ff50f0c..00000000 --- a/samples/angular/src/app/backend/services/video-game.service.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; -import { VideoGameService } from './video-game.service'; -import { VideoGame } from '../types/video-game'; -import { environment } from 'src/environments/environment.dev'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; - -describe('VideoGameService', () => { - let service: VideoGameService; - let http: HttpTestingController; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [], - providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] -}); - http = TestBed.inject(HttpTestingController); - service = TestBed.inject(VideoGameService); - }); - - afterAll(() => http.verify()); - - it('should list', () => { - // fake response - const fake = [{ title: 'Final Fantasy VII' }, { title: 'Resident Evil' }] as Array; - - let actual: Array = []; - service.list().subscribe((movies: Array) => actual = movies); - - http.expectOne(`${environment.keepTrackApiUrl}/api/video-games?search=&platform=&state=&page=0&pageSize=50`) - .flush(fake); - - expect(actual).toEqual(fake, 'The `list` method should return an array of TV Shows wrapped in an Observable'); - }); -}); diff --git a/samples/angular/src/app/backend/services/video-game.service.ts b/samples/angular/src/app/backend/services/video-game.service.ts deleted file mode 100644 index 01df6aae..00000000 --- a/samples/angular/src/app/backend/services/video-game.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; -import { Observable } from 'rxjs'; -import { environment } from 'src/environments/environment'; -import { VideoGame } from '../types/video-game'; -import { DataService } from './data.interface'; - -@Injectable({ - providedIn: 'root' -}) -export class VideoGameService implements DataService { - private httpClient = inject(HttpClient); - - get(id: string): Observable { - return this.httpClient.get(`${environment.keepTrackApiUrl}/api/video-games/${id}`); - } - - list(search?: string, currentPage?: number, pageSize?: number, filter?: VideoGame): Observable> { - return this.httpClient.get>(`${environment.keepTrackApiUrl}/api/video-games?search=${search ?? ''}&platform=${filter?.platform ?? ''}&state=${filter?.state ?? ''}&page=${currentPage ?? 0}&pageSize=${pageSize ?? 50}`); - } - - create(input: VideoGame): Observable { - return this.httpClient.post(`${environment.keepTrackApiUrl}/api/video-games`, input); - } - - update(input: VideoGame): Observable { - delete input.isEditable; - if (!input.finishedAt) { - delete input.finishedAt; - } - if (!input.releasedAt) { - delete input.releasedAt; - } - return this.httpClient.put(`${environment.keepTrackApiUrl}/api/video-games/${input.id}`, input); - } - - delete(input: VideoGame): Observable { - return this.httpClient.delete(`${environment.keepTrackApiUrl}/api/video-games/${input.id}`); - } -} diff --git a/samples/angular/src/app/backend/types/backend-data.d.ts b/samples/angular/src/app/backend/types/backend-data.d.ts deleted file mode 100644 index 54ce53d7..00000000 --- a/samples/angular/src/app/backend/types/backend-data.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface BackendData { - id?: string; - isEditable?: boolean; -} diff --git a/samples/angular/src/app/backend/types/book.d.ts b/samples/angular/src/app/backend/types/book.d.ts deleted file mode 100644 index 8849e68f..00000000 --- a/samples/angular/src/app/backend/types/book.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BackendData } from "./backend-data"; - -export interface Book extends BackendData { - id?: string; - title?: string; - author?: string; - series?: string; - finishedAt?: Date; - isEditable?: boolean; -} diff --git a/samples/angular/src/app/backend/types/car-history.d.ts b/samples/angular/src/app/backend/types/car-history.d.ts deleted file mode 100644 index 28e2c670..00000000 --- a/samples/angular/src/app/backend/types/car-history.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { BackendData } from "./backend-data"; - -export interface CarHistory extends BackendData { - id?: string; - carId?: string; - historyDate?: Date; - mileage?: number; - action?: string; - city?: string; - longitude?: number; - latitude?: number; - fuelCategory?: string; - fuelVolume?: number; - fuelUnitPrice?: number; - amount?: number; - isFullTank?: boolean; - deltaMileage?: number; - lastRefuelHistoryId?: string; - stationBrandName?: string; -} diff --git a/samples/angular/src/app/backend/types/car.d.ts b/samples/angular/src/app/backend/types/car.d.ts deleted file mode 100644 index 0392e2e2..00000000 --- a/samples/angular/src/app/backend/types/car.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { BackendData } from "./backend-data"; - -export interface Car extends BackendData { - id?: string; - name?: string; -} diff --git a/samples/angular/src/app/backend/types/movie.d.ts b/samples/angular/src/app/backend/types/movie.d.ts deleted file mode 100644 index b179aaa1..00000000 --- a/samples/angular/src/app/backend/types/movie.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BackendData } from "./backend-data"; - -export interface Movie extends BackendData { - id?: string; - title?: string; - year?: number; - imdbPageId?: string; - allocineId?: string; - isEditable?: boolean; -} diff --git a/samples/angular/src/app/backend/types/tv-show.d.ts b/samples/angular/src/app/backend/types/tv-show.d.ts deleted file mode 100644 index 512d92c1..00000000 --- a/samples/angular/src/app/backend/types/tv-show.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { BackendData } from "./backend-data"; - -export interface TvShow extends BackendData { - id?: string; - title?: string; - isEditable?: boolean; -} diff --git a/samples/angular/src/app/backend/types/video-game.d.ts b/samples/angular/src/app/backend/types/video-game.d.ts deleted file mode 100644 index 800fcbe1..00000000 --- a/samples/angular/src/app/backend/types/video-game.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { BackendData } from "./backend-data"; - -export interface VideoGame extends BackendData { - id?: string; - title?: string; - platform?: string; - releasedAt?: Date; - state?: string; - finishedAt?: Date; - isEditable?: boolean; -} diff --git a/samples/angular/src/app/home/home.component.html b/samples/angular/src/app/home/home.component.html deleted file mode 100644 index 59ba4617..00000000 --- a/samples/angular/src/app/home/home.component.html +++ /dev/null @@ -1,9 +0,0 @@ -

Welcome!

-

Welcome to your application to keep track of things. So far, we can manage:

-
    -
  • Books
  • -
  • Cars
  • -
  • Movies
  • -
  • Video games
  • -
  • TV shows
  • -
diff --git a/samples/angular/src/app/home/home.component.ts b/samples/angular/src/app/home/home.component.ts deleted file mode 100644 index 4d02ae5d..00000000 --- a/samples/angular/src/app/home/home.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from '@angular/core'; -import { FormsModule } from "@angular/forms"; - -@Component({ - selector: 'app-home', - imports: [FormsModule], - templateUrl: './home.component.html' -}) -export class HomeComponent { -} diff --git a/samples/angular/src/app/interceptors/auth.interceptor.ts b/samples/angular/src/app/interceptors/auth.interceptor.ts deleted file mode 100644 index 5c7f6c35..00000000 --- a/samples/angular/src/app/interceptors/auth.interceptor.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { HttpInterceptorFn } from '@angular/common/http'; -import { inject } from '@angular/core'; -import { from, switchMap } from 'rxjs'; -import { Auth, idToken } from '@angular/fire/auth'; -import { environment } from "../../environments/environment"; - -export const authInterceptor: HttpInterceptorFn = (req, next) => { - if (!req.url.startsWith(environment.keepTrackApiUrl)) { - return next(req); - } - - return from(idToken(inject(Auth))).pipe( - switchMap(token => { - if (token) { - // eslint-disable-next-line @typescript-eslint/naming-convention - return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })); - } - return next(req); - }) - ); -}; diff --git a/samples/angular/src/app/inventory/base/data.component.ts b/samples/angular/src/app/inventory/base/data.component.ts deleted file mode 100644 index 2c8258c5..00000000 --- a/samples/angular/src/app/inventory/base/data.component.ts +++ /dev/null @@ -1,81 +0,0 @@ -import {Directive, inject, OnDestroy, OnInit} from '@angular/core'; -import { Subscription } from 'rxjs'; -import { AuthenticateService } from 'src/app/user/services/authenticate.service'; -import { DataService } from 'src/app/backend/services/data.interface'; -import { BackendData } from 'src/app/backend/types/backend-data'; - -@Directive() -export abstract class DataComponent implements OnInit, OnDestroy { - protected abstract readonly dataService: DataService; - private authenticateService = inject(AuthenticateService); - userEventsSubscription: Subscription | undefined; - items: Array | undefined; - currentPage = 1; - pageSize = 50; - - ngOnInit() { - this.userEventsSubscription = this.authenticateService.authState$.subscribe(() => { - this.load('', 1); - }); - } - - ngOnDestroy() { - if (this.userEventsSubscription) { - this.userEventsSubscription.unsubscribe(); - } - } - - load(search: string, currentPage: number, filter?: T) { - this.currentPage = currentPage; - this.dataService.list(search, currentPage - 1, this.pageSize, filter).subscribe({ - next: (items) => this.items = items, - error: (error) => console.warn(error) - }); - } - - updateCurrentPage(event: Event, newValue: number, search?: string) { - event.preventDefault(); - if (newValue > 0) { - this.load(search ?? '', newValue); - } - } - - create(item: T) { - this.dataService.create(item).subscribe(created => { - this.items?.push(created); - this.resetInputFields(); - }); - } - - abstract resetInputFields(): void; - - startEditing(item: T) { - item.isEditable = true; - } - - cancel(item: T) { - item.isEditable = false; - - if (!item.id) { - return; - } - - this.dataService.get(item.id).subscribe(existing => { - item = existing; - }); - } - - update(item: T) { - this.dataService.update(item) - .subscribe(() => item.isEditable = false); - } - - delete(item: T) { - this.dataService.delete(item) - .subscribe(() => { - if (this.items) { - this.items.splice(this.items.findIndex(x => x.id === item.id), 1); - } - }); - } -} diff --git a/samples/angular/src/app/inventory/book/book.component.html b/samples/angular/src/app/inventory/book/book.component.html deleted file mode 100644 index e46a1b60..00000000 --- a/samples/angular/src/app/inventory/book/book.component.html +++ /dev/null @@ -1,129 +0,0 @@ -

Books

- -@if (!items) { -

Loading...

-} - -
- -
- -
-
- -
- @if (items) { - - - - - - - - - - - - @for (book of items; track book; let i = $index) { - - - - - - - - } - -
TitleAuthor(s)SeriesFinished dateActions
- @if (!book.isEditable ) { - {{ book.title }} - } - @if (book.isEditable) { -
- -
- } -
- @if (!book.isEditable ) { - {{ book.author }} - } - @if (book.isEditable) { -
- -
- } -
- @if (!book.isEditable ) { - {{ book.series }} - } - @if (book.isEditable) { -
- -
- } -
- @if (!book.isEditable ) { - {{ book.finishedAt | date: 'yyyy-MM-dd' }} - } - @if (book.isEditable) { -
- -
- } -
- @if (!book.isEditable) { - - } - @if (book.isEditable) { - - } - @if (book.isEditable) { - - } - @if (!book.isEditable) { - - } -
- } -
- -@if (items) { - -} - -@if (items) { -
-
-
-

Add

-
-
-
-
- - -
-
- - -
-
- - -
-
- -
- -
-
-
-
-
-} diff --git a/samples/angular/src/app/inventory/book/book.component.spec.ts b/samples/angular/src/app/inventory/book/book.component.spec.ts deleted file mode 100644 index 6145e221..00000000 --- a/samples/angular/src/app/inventory/book/book.component.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { User } from '@angular/fire/auth'; -import { Observable } from 'rxjs'; - -import { AuthenticateService } from 'src/app/user/services/authenticate.service'; -import { BookComponent } from './book.component'; -import { BookService } from 'src/app/backend/services/book.service'; - -describe('BookComponent', () => { - let fakeBookService: jasmine.SpyObj; - let fakeAuthenticateService: jasmine.SpyObj; - - beforeEach(() => { - const emptyUser = new Observable(); - fakeBookService = jasmine.createSpyObj('BookService', ['list']); - fakeAuthenticateService = jasmine.createSpyObj('AuthenticateService', [], { 'authState$': emptyUser }); - - TestBed.configureTestingModule({ - imports: [], - providers: [ - { provide: BookService, useValue: fakeBookService }, - { provide: AuthenticateService, useValue: fakeAuthenticateService } - ] - }); - }); - - it('should listen to userEvents in ngOnInit', waitForAsync(() => { - const fixture = TestBed.createComponent(BookComponent); - const component = fixture.componentInstance; - component.ngOnInit(); - })); -}); diff --git a/samples/angular/src/app/inventory/book/book.component.ts b/samples/angular/src/app/inventory/book/book.component.ts deleted file mode 100644 index 7295ec81..00000000 --- a/samples/angular/src/app/inventory/book/book.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Component, OnInit, OnDestroy, ElementRef, ViewChild, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { BookService } from 'src/app/backend/services/book.service'; -import { Book } from 'src/app/backend/types/book'; -import { DataComponent } from '../base/data.component'; - -@Component({ - selector: 'app-book', - imports: [CommonModule, FormsModule], - templateUrl: './book.component.html' -}) -export class BookComponent extends DataComponent implements OnInit, OnDestroy { - protected override readonly dataService = inject(BookService); - - @ViewChild('titleInput') titleInput = {} as ElementRef; - @ViewChild('authorInput') authorInput = {} as ElementRef; - @ViewChild('seriesInput') seriesInput = {} as ElementRef; - @ViewChild('searchInput') searchInput = {} as ElementRef; - - resetInputFields() { - this.titleInput.nativeElement.value = ''; - this.authorInput.nativeElement.value = ''; - this.seriesInput.nativeElement.value = ''; - } -} diff --git a/samples/angular/src/app/inventory/car/car.component.html b/samples/angular/src/app/inventory/car/car.component.html deleted file mode 100644 index 7608405e..00000000 --- a/samples/angular/src/app/inventory/car/car.component.html +++ /dev/null @@ -1 +0,0 @@ -

car works!

diff --git a/samples/angular/src/app/inventory/car/car.component.spec.ts b/samples/angular/src/app/inventory/car/car.component.spec.ts deleted file mode 100644 index ce22e0cb..00000000 --- a/samples/angular/src/app/inventory/car/car.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; - -import { CarComponent } from './car.component'; - -describe('CarComponent', () => { - let component: CarComponent; - let fixture: ComponentFixture; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [ CarComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(CarComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/samples/angular/src/app/inventory/car/car.component.ts b/samples/angular/src/app/inventory/car/car.component.ts deleted file mode 100644 index e3f9e46a..00000000 --- a/samples/angular/src/app/inventory/car/car.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from '@angular/core'; -import { FormsModule } from "@angular/forms"; - -@Component({ - selector: 'app-car', - imports: [FormsModule], - templateUrl: './car.component.html' -}) -export class CarComponent { -} diff --git a/samples/angular/src/app/inventory/inventory.module.ts b/samples/angular/src/app/inventory/inventory.module.ts deleted file mode 100644 index ede9e107..00000000 --- a/samples/angular/src/app/inventory/inventory.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { MovieComponent } from './movie/movie.component'; -import { BookComponent } from './book/book.component'; -import { CarComponent } from './car/car.component'; -import { VideoGameComponent } from './video-game/video-game.component'; -import { TvShowComponent } from './tv-show/tv-show.component'; -import { FormsModule } from '@angular/forms'; - -@NgModule({ - declarations: [ - MovieComponent, - BookComponent, - CarComponent, - VideoGameComponent, - TvShowComponent - ], - imports: [ - CommonModule, - FormsModule - ] -}) -export class InventoryModule { } diff --git a/samples/angular/src/app/inventory/movie/movie.component.html b/samples/angular/src/app/inventory/movie/movie.component.html deleted file mode 100644 index 85011b6e..00000000 --- a/samples/angular/src/app/inventory/movie/movie.component.html +++ /dev/null @@ -1,104 +0,0 @@ -

Movies

- -@if (!items) { -

Loading...

-} - -
- -
- -
-
- -
- @if (items) { - - - - - - - - - - @for (movie of items; track movie) { - - - - - - } - -
TitleYearActions
- @if (!movie.isEditable ) { - {{ movie.title }} - } - @else { -
- -
- } -
- @if (!movie.isEditable ) { - {{ movie.year }} - } - @else { -
- -
- } -
- @if (!movie.isEditable) { - - - } - @else { - - - } -
- } -
- -@if (items) { - -
-
-
-

Add

-
-
-
-
- - -
-
- - -
-
- -
- -
-
-
-
-
-} diff --git a/samples/angular/src/app/inventory/movie/movie.component.spec.ts b/samples/angular/src/app/inventory/movie/movie.component.spec.ts deleted file mode 100644 index 008b1aff..00000000 --- a/samples/angular/src/app/inventory/movie/movie.component.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { User } from '@angular/fire/auth'; -import { Observable } from 'rxjs'; - -import { AuthenticateService } from 'src/app/user/services/authenticate.service'; -import { MovieComponent } from './movie.component'; -import { MovieService } from 'src/app/backend/services/movie.service'; - -describe('MovieComponent', () => { - let fakeMovieService: jasmine.SpyObj; - let fakeAuthenticateService: jasmine.SpyObj; - - beforeEach(() => { - const emptyUser = new Observable(); - fakeMovieService = jasmine.createSpyObj('MovieService', ['list']); - fakeAuthenticateService = jasmine.createSpyObj('AuthenticateService', [], { 'authState$': emptyUser }); - - TestBed.configureTestingModule({ - imports: [], - providers: [ - { provide: MovieService, useValue: fakeMovieService }, - { provide: AuthenticateService, useValue: fakeAuthenticateService } - ] - }); - }); - - it('should listen to userEvents in ngOnInit', waitForAsync(() => { - const fixture = TestBed.createComponent(MovieComponent); - const component = fixture.componentInstance; - component.ngOnInit(); - })); -}); diff --git a/samples/angular/src/app/inventory/movie/movie.component.ts b/samples/angular/src/app/inventory/movie/movie.component.ts deleted file mode 100644 index cb0f3ed0..00000000 --- a/samples/angular/src/app/inventory/movie/movie.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Component, OnInit, OnDestroy, ViewChild, ElementRef, inject } from '@angular/core'; -import { CommonModule } from "@angular/common"; -import { FormsModule } from "@angular/forms"; -import { MovieService } from 'src/app/backend/services/movie.service'; -import { Movie } from 'src/app/backend/types/movie'; -import { DataComponent } from '../base/data.component'; - -@Component({ - selector: 'app-movie', - imports: [CommonModule, FormsModule], - templateUrl: './movie.component.html' -}) -export class MovieComponent extends DataComponent implements OnInit, OnDestroy { - protected override readonly dataService = inject(MovieService); - - @ViewChild('titleInput') titleInput= {} as ElementRef; - @ViewChild('yearInput') yearInput= {} as ElementRef; - - resetInputFields() { - this.titleInput.nativeElement.value = ''; - this.yearInput.nativeElement.value = ''; - } -} diff --git a/samples/angular/src/app/inventory/tv-show/tv-show.component.html b/samples/angular/src/app/inventory/tv-show/tv-show.component.html deleted file mode 100644 index ae3298fc..00000000 --- a/samples/angular/src/app/inventory/tv-show/tv-show.component.html +++ /dev/null @@ -1,59 +0,0 @@ -

TV shows

- -@if (!items) { -

Loading...

-} - -
- -
- -
-
- -@if (items) { - - - - - - - - - @for (tvShow of items; track tvShow) { - - - - - } - -
TitleActions
{{ tvShow.title }}
-} - -@if (items) { - -
-
-

Add

-
-
-
-
- - -
-
- -
- -
-
-
-
-} diff --git a/samples/angular/src/app/inventory/tv-show/tv-show.component.spec.ts b/samples/angular/src/app/inventory/tv-show/tv-show.component.spec.ts deleted file mode 100644 index 9c5ab05f..00000000 --- a/samples/angular/src/app/inventory/tv-show/tv-show.component.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { User } from '@angular/fire/auth'; -import { Observable } from 'rxjs'; - -import { AppModule } from 'src/app/app.module'; -import { AuthenticateService } from 'src/app/user/services/authenticate.service'; -import { TvShowComponent } from './tv-show.component'; -import { TvShowService } from 'src/app/backend/services/tv-show.service'; - -describe('TvShowComponent', () => { - let fakeTvShowService: jasmine.SpyObj; - let fakeAuthenticateService: jasmine.SpyObj; - - beforeEach(() => { - const emptyUser = new Observable(); - fakeTvShowService = jasmine.createSpyObj('TvShowService', ['list']); - fakeAuthenticateService = jasmine.createSpyObj('AuthenticateService', [], { 'authState$': emptyUser }); - - TestBed.configureTestingModule({ - imports: [AppModule], - providers: [ - { provide: TvShowService, useValue: fakeTvShowService }, - { provide: AuthenticateService, useValue: fakeAuthenticateService } - ] - }); - }); - - it('should listen to userEvents in ngOnInit', waitForAsync(() => { - const fixture = TestBed.createComponent(TvShowComponent); - const component = fixture.componentInstance; - component.ngOnInit(); - })); -}); diff --git a/samples/angular/src/app/inventory/tv-show/tv-show.component.ts b/samples/angular/src/app/inventory/tv-show/tv-show.component.ts deleted file mode 100644 index bb44ed05..00000000 --- a/samples/angular/src/app/inventory/tv-show/tv-show.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Component, ElementRef, OnDestroy, OnInit, ViewChild, inject } from '@angular/core'; -import { CommonModule } from "@angular/common"; -import { FormsModule } from "@angular/forms"; -import { TvShowService } from 'src/app/backend/services/tv-show.service'; -import { TvShow } from 'src/app/backend/types/tv-show'; -import { DataComponent } from '../base/data.component'; - -@Component({ - selector: 'app-tv-show', - imports: [CommonModule, FormsModule], - templateUrl: './tv-show.component.html' -}) -export class TvShowComponent extends DataComponent implements OnInit, OnDestroy { - protected override readonly dataService = inject(TvShowService); - - @ViewChild('titleInput') titleInput= {} as ElementRef; - - resetInputFields() { - this.titleInput.nativeElement.value = ''; - } -} diff --git a/samples/angular/src/app/inventory/video-game/video-game.component.html b/samples/angular/src/app/inventory/video-game/video-game.component.html deleted file mode 100644 index 7f8694a0..00000000 --- a/samples/angular/src/app/inventory/video-game/video-game.component.html +++ /dev/null @@ -1,175 +0,0 @@ -

Video games

- -@if (!items) { -

Loading...

-} - -
- -
- -
-
- -
-
- -
-
- -
- @if (items) { - - - - - - - - - - - - @for (videoGame of items; track videoGame) { - - - - - - - - } - -
TitlePlatformStateFinished dateActions
- @if (!videoGame.isEditable ) { - {{ videoGame.title }} - } - @else { -
- -
- } -
- @if (!videoGame.isEditable ) { - {{ videoGame.platform }} - } - @else { -
- -
- } -
- @if (!videoGame.isEditable ) { - {{ videoGame.state }} - } - @else { -
- -
- } -
- @if (!videoGame.isEditable ) { - {{ videoGame.finishedAt | date: 'yyyy-MM-dd' }} - } - @else { -
- -
- } -
- @if (!videoGame.isEditable) { - - - } - @else { - - - } -
- } -
- -@if (items) { - -} - -@if (items) { -
-
-
-

Add

-
-
-
-
- - -
-
- - -
-
- - -
-
- -
- -
-
-
-
-
-} diff --git a/samples/angular/src/app/inventory/video-game/video-game.component.spec.ts b/samples/angular/src/app/inventory/video-game/video-game.component.spec.ts deleted file mode 100644 index d9ce95a6..00000000 --- a/samples/angular/src/app/inventory/video-game/video-game.component.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { User } from '@angular/fire/auth'; -import { Observable } from 'rxjs'; - -import { AuthenticateService } from 'src/app/user/services/authenticate.service'; -import { VideoGameComponent } from './video-game.component'; -import { VideoGameService } from 'src/app/backend/services/video-game.service'; - -describe('VideoGameComponent', () => { - let fakeVideoGameService: jasmine.SpyObj; - let fakeAuthenticateService: jasmine.SpyObj; - - beforeEach(() => { - const emptyUser = new Observable(); - fakeVideoGameService = jasmine.createSpyObj('VideoGameService', ['list']); - fakeAuthenticateService = jasmine.createSpyObj('AuthenticateService', [], { 'authState$': emptyUser }); - - TestBed.configureTestingModule({ - imports: [], - providers: [ - { provide: VideoGameService, useValue: fakeVideoGameService }, - { provide: AuthenticateService, useValue: fakeAuthenticateService } - ] - }); - }); - - it('should listen to userEvents in ngOnInit', waitForAsync(() => { - const fixture = TestBed.createComponent(VideoGameComponent); - const component = fixture.componentInstance; - component.ngOnInit(); - })); -}); diff --git a/samples/angular/src/app/inventory/video-game/video-game.component.ts b/samples/angular/src/app/inventory/video-game/video-game.component.ts deleted file mode 100644 index b5047879..00000000 --- a/samples/angular/src/app/inventory/video-game/video-game.component.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Component, ElementRef, OnDestroy, OnInit, ViewChild, inject } from '@angular/core'; -import { CommonModule } from "@angular/common"; -import { FormsModule } from "@angular/forms"; -import { VideoGameService } from 'src/app/backend/services/video-game.service'; -import { VideoGame } from 'src/app/backend/types/video-game'; -import { DataComponent } from '../base/data.component'; - -@Component({ - selector: 'app-video-game', - imports: [CommonModule, FormsModule], - templateUrl: './video-game.component.html' -}) -export class VideoGameComponent extends DataComponent implements OnInit, OnDestroy { - protected override readonly dataService = inject(VideoGameService); - - @ViewChild('titleInput') titleInput = {} as ElementRef; - @ViewChild('platformInput') platformInput= {} as ElementRef; - @ViewChild('stateInput') stateInput= {} as ElementRef; - - resetInputFields() { - this.titleInput.nativeElement.value = ''; - this.platformInput.nativeElement.value = ''; - this.stateInput.nativeElement.value = ''; - } -} diff --git a/samples/angular/src/app/layout/header/header.component.css b/samples/angular/src/app/layout/header/header.component.css deleted file mode 100644 index 10389ef9..00000000 --- a/samples/angular/src/app/layout/header/header.component.css +++ /dev/null @@ -1,18 +0,0 @@ -a.navbar-brand { - white-space: normal; - text-align: center; - word-break: break-all; -} - -html { - font-size: 14px; -} -@media (min-width: 768px) { - html { - font-size: 16px; - } -} - -.box-shadow { - box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); -} diff --git a/samples/angular/src/app/layout/header/header.component.html b/samples/angular/src/app/layout/header/header.component.html deleted file mode 100644 index c51e253a..00000000 --- a/samples/angular/src/app/layout/header/header.component.html +++ /dev/null @@ -1,45 +0,0 @@ -
- -
diff --git a/samples/angular/src/app/layout/header/header.component.ts b/samples/angular/src/app/layout/header/header.component.ts deleted file mode 100644 index fcb40801..00000000 --- a/samples/angular/src/app/layout/header/header.component.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Component, OnInit, OnDestroy, inject } from '@angular/core'; -import { User } from '@angular/fire/auth'; -import { Router, RouterLink, RouterLinkActive } from '@angular/router'; -import { CommonModule } from "@angular/common"; -import { Subscription } from 'rxjs'; -import { AuthenticateService } from 'src/app/user/services/authenticate.service'; - -@Component({ - selector: 'app-header', - imports: [ - CommonModule, - RouterLink, - RouterLinkActive - ], - templateUrl: './header.component.html', - styleUrls: ['./header.component.css'] -}) -export class HeaderComponent implements OnInit, OnDestroy { - private authenticateService = inject(AuthenticateService); - private router = inject(Router); - - isExpanded = false; - - user = null as User | null; - userEventsSubscription: Subscription | undefined; - - ngOnInit() { - this.userEventsSubscription = this.authenticateService.authState$.subscribe({ - next: (user: User | null) => this.user = user, - error: (error) => console.log(error) - }); - } - - ngOnDestroy() { - if (this.userEventsSubscription) { - this.userEventsSubscription.unsubscribe(); - } - } - - async logout(event: Event) { - event.preventDefault(); - await this.authenticateService.logout(); - await this.router.navigate(['/login']); - } - - collapse() { - this.isExpanded = false; - } - - toggle() { - this.isExpanded = !this.isExpanded; - } - -} diff --git a/samples/angular/src/app/layout/layout.module.ts b/samples/angular/src/app/layout/layout.module.ts deleted file mode 100644 index 10fcaf58..00000000 --- a/samples/angular/src/app/layout/layout.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; - -@NgModule({ - declarations: [ ], - imports: [ - CommonModule, - RouterModule - ] -}) -export class LayoutModule { } diff --git a/samples/angular/src/app/user/login/login.component.html b/samples/angular/src/app/user/login/login.component.html deleted file mode 100644 index 2287fd8e..00000000 --- a/samples/angular/src/app/user/login/login.component.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/samples/angular/src/app/user/login/login.component.spec.ts b/samples/angular/src/app/user/login/login.component.spec.ts deleted file mode 100644 index 41140adf..00000000 --- a/samples/angular/src/app/user/login/login.component.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { LoginComponent } from './login.component'; -import { AuthenticateService } from '../services/authenticate.service'; - -describe('LoginComponent', () => { - - const fakeAuthenticateService = jasmine.createSpyObj('AuthenticateService', ['signInWithGitHub']); - - beforeEach(() => TestBed.configureTestingModule({ - imports: [], - providers: [ - { provide: AuthenticateService, useValue: fakeAuthenticateService } - ] - })); - - beforeEach(() => { - fakeAuthenticateService.signInWithGitHub.calls.reset(); - }); - - it('should have a title', () => { - const fixture = TestBed.createComponent(LoginComponent); - - // when we trigger the change detection - fixture.detectChanges(); - - // then we should have a title - const element = fixture.nativeElement; - expect(element.querySelector('button')).not.toBeNull('The template should have a `h1` tag'); - }); -}); diff --git a/samples/angular/src/app/user/login/login.component.ts b/samples/angular/src/app/user/login/login.component.ts deleted file mode 100644 index b3e3101d..00000000 --- a/samples/angular/src/app/user/login/login.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component, inject } from '@angular/core'; -import { Router } from '@angular/router'; -import { AuthenticateService } from '../services/authenticate.service'; - -@Component({ - selector: 'app-login', - templateUrl: './login.component.html' -}) -export class LoginComponent { - private authenticateService = inject(AuthenticateService); - private router = inject(Router); - - async signInWithGitHub() { - await this.authenticateService.signInWithGitHub(); - await this.router.navigate(['/']); - } -} diff --git a/samples/angular/src/app/user/models/user.model.ts b/samples/angular/src/app/user/models/user.model.ts deleted file mode 100644 index 88056c0a..00000000 --- a/samples/angular/src/app/user/models/user.model.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface UserModel { - username: string; - token?: string; -} diff --git a/samples/angular/src/app/user/services/authenticate.service.spec.ts b/samples/angular/src/app/user/services/authenticate.service.spec.ts deleted file mode 100644 index 2005ee7c..00000000 --- a/samples/angular/src/app/user/services/authenticate.service.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; -import { AngularFireModule } from '@angular/fire/compat'; -import { AngularFireAuthModule } from '@angular/fire/compat/auth'; -import { AuthenticateService } from './authenticate.service'; -import { environment } from 'src/environments/environment.dev'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; - -describe('AuthenticateService', () => { - let authenticateService: AuthenticateService; - let http: HttpTestingController; - - beforeEach(() => TestBed.configureTestingModule({ - imports: [ - AngularFireModule.initializeApp(environment.firebase), - AngularFireAuthModule], - providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] -})); - - beforeEach(() => { - http = TestBed.inject(HttpTestingController); - authenticateService = TestBed.inject(AuthenticateService); - }); - - afterAll(() => http.verify()); - - it('should logout', () => { - authenticateService.logout(); - }); -}); diff --git a/samples/angular/src/app/user/services/authenticate.service.ts b/samples/angular/src/app/user/services/authenticate.service.ts deleted file mode 100644 index bfb694b2..00000000 --- a/samples/angular/src/app/user/services/authenticate.service.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { Auth, GithubAuthProvider, authState, signInWithPopup } from '@angular/fire/auth'; - -// see https://github.com/angular/angularfire/blob/main/docs/auth.md -@Injectable({ - providedIn: 'root' -}) -export class AuthenticateService { - private auth: Auth = inject(Auth); - authState$ = authState(this.auth); - - // see https://firebase.google.com/docs/auth/web/github-auth - async signInWithGitHub() { - try { - await signInWithPopup(this.auth, new GithubAuthProvider()); - } catch (error) { - console.log(error); - } - } - - async logout() { - await this.auth.signOut(); - } -} diff --git a/samples/angular/src/app/user/user.module.ts b/samples/angular/src/app/user/user.module.ts deleted file mode 100644 index 06108402..00000000 --- a/samples/angular/src/app/user/user.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { AuthenticateService } from './services/authenticate.service'; -import { LoginComponent } from './login/login.component'; - -@NgModule({ - declarations: [LoginComponent], - imports: [ - CommonModule - ], - providers: [ - AuthenticateService - ] -}) -export class UserModule { } diff --git a/samples/angular/src/assets/.gitkeep b/samples/angular/src/assets/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/samples/angular/src/environments/environment.ts b/samples/angular/src/environments/environment.ts deleted file mode 100644 index b25fe8c1..00000000 --- a/samples/angular/src/environments/environment.ts +++ /dev/null @@ -1,26 +0,0 @@ -// This file can be replaced during build by using the `fileReplacements` array. -// `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. -// The list of file replacements can be found in `angular.json`. - -export const environment = { - production: false, - keepTrackApiUrl: '', - firebase: { - apiKey: '', - authDomain: '', - databaseURL: '', - projectId: '', - storageBucket: '', - messagingSenderId: '', - appId: '', - measurementId: '' - } -}; - -/* - * In development mode, to ignore zone related error stack frames such as - * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can - * import the following file, but please comment it out in production mode - * because it will have performance impact when throw error - */ -// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/samples/angular/src/favicon.ico b/samples/angular/src/favicon.ico deleted file mode 100644 index 997406ad..00000000 Binary files a/samples/angular/src/favicon.ico and /dev/null differ diff --git a/samples/angular/src/index.html b/samples/angular/src/index.html deleted file mode 100644 index a0ac61e7..00000000 --- a/samples/angular/src/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Keeptrack - - - - - - Loading... - - diff --git a/samples/angular/src/main.ts b/samples/angular/src/main.ts deleted file mode 100644 index 5888d1ae..00000000 --- a/samples/angular/src/main.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { enableProdMode, provideZoneChangeDetection } from '@angular/core'; -import { bootstrapApplication } from '@angular/platform-browser'; -import { provideRouter } from '@angular/router'; -import { provideHttpClient, withInterceptors } from '@angular/common/http'; -import { provideFirebaseApp, initializeApp } from '@angular/fire/app'; -import { provideAuth, getAuth } from '@angular/fire/auth'; - -import { AppComponent } from './app/app.component'; -import { routes } from './app/app.routes'; -import { authInterceptor } from "./app/interceptors/auth.interceptor"; -import { environment } from './environments/environment'; - -if (environment.production) { - enableProdMode(); -} - -bootstrapApplication(AppComponent, { - providers: [ - provideZoneChangeDetection(),provideRouter(routes), - provideHttpClient(withInterceptors([authInterceptor])), - provideFirebaseApp(() => initializeApp(environment.firebase)), - provideAuth(() => getAuth()), - provideHttpClient( - withInterceptors([authInterceptor]) - ) - ] -}).catch(err => console.error(err)); diff --git a/samples/angular/src/styles.css b/samples/angular/src/styles.css deleted file mode 100644 index 9e5d916d..00000000 --- a/samples/angular/src/styles.css +++ /dev/null @@ -1,18 +0,0 @@ -/* You can add global styles to this file, and also import other style files */ - -@import 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'; - -/* Provide sufficient contrast against white background */ -a { - color: #0366d6; -} - -code { - color: #e01a76; -} - -.btn-primary { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} diff --git a/samples/angular/src/web.config b/samples/angular/src/web.config deleted file mode 100644 index 5ec2c9d6..00000000 --- a/samples/angular/src/web.config +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/angular/tsconfig.app.json b/samples/angular/tsconfig.app.json deleted file mode 100644 index 7d7c716d..00000000 --- a/samples/angular/tsconfig.app.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./out-tsc/app", - "types": [] - }, - "files": [ - "src/main.ts" - ], - "include": [ - "src/**/*.d.ts" - ] -} diff --git a/samples/angular/tsconfig.json b/samples/angular/tsconfig.json deleted file mode 100644 index 213dba09..00000000 --- a/samples/angular/tsconfig.json +++ /dev/null @@ -1,33 +0,0 @@ -/* Ref. https://angular.io/config/tsconfig */ -{ - "compileOnSave": false, - "compilerOptions": { - "baseUrl": "./", - "outDir": "./dist/out-tsc", - "forceConsistentCasingInFileNames": true, - "esModuleInterop": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, - "sourceMap": true, - "declaration": false, - "experimentalDecorators": true, - "moduleResolution": "bundler", - "importHelpers": true, - "target": "ES2022", - "module": "ES2022", - "useDefineForClassFields": false - }, - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true, - "extendedDiagnostics": { - "defaultCategory": "error" - } - } -} diff --git a/samples/angular/tsconfig.spec.json b/samples/angular/tsconfig.spec.json deleted file mode 100644 index b18619fd..00000000 --- a/samples/angular/tsconfig.spec.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./out-tsc/spec", - "types": [ - "jasmine" - ] - }, - "include": [ - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] -} diff --git a/scripts/mongodb-create-index.js b/scripts/mongodb-create-index.js index f56043ea..dba3c20b 100644 --- a/scripts/mongodb-create-index.js +++ b/scripts/mongodb-create-index.js @@ -26,6 +26,8 @@ ensureIndex(db.house, { owner_id: 1 }, { name: "house_owner" }); ensureIndex(db.house_history, { owner_id: 1 }, { name: "house_history_owner" }); ensureIndex(db.health_profile, { owner_id: 1 }, { name: "health_profile_owner" }); ensureIndex(db.health_record, { owner_id: 1 }, { name: "health_record_owner" }); +ensureIndex(db.collectible, { owner_id: 1 }, { name: "collectible_owner" }); +ensureIndex(db.gear, { owner_id: 1 }, { name: "gear_owner" }); ensureIndex(db.movie, { owner_id: 1 }, { name: "movie_owner" }); ensureIndex(db.tvshow, { owner_id: 1 }, { name: "tvshow_owner" }); ensureIndex(db.videogame, { owner_id: 1 }, { name: "videogame_owner" }); @@ -91,6 +93,16 @@ ensureIndex( { owner_id: 1, is_favorite: 1 }, { name: "book_favorite", partialFilterExpression: { is_favorite: true } } ); +ensureIndex( + db.collectible, + { owner_id: 1, is_favorite: 1 }, + { name: "collectible_favorite", partialFilterExpression: { is_favorite: true } } +); +ensureIndex( + db.gear, + { owner_id: 1, is_favorite: 1 }, + { name: "gear_favorite", partialFilterExpression: { is_favorite: true } } +); // movie / tvshow / book / videogame: same sparse-flag partial-index rationale as the favorite/want-to-watch // indexes above, for the is_wishlisted flag. VideoGame has no favorite/want-to-watch flags, so this is its @@ -147,6 +159,16 @@ ensureIndex( { owner_id: 1 }, { name: "videogame_owned", partialFilterExpression: { "platforms.0": { $exists: true } } } ); +ensureIndex( + db.collectible, + { owner_id: 1 }, + { name: "collectible_owned", partialFilterExpression: { "owned_versions.0": { $exists: true } } } +); +ensureIndex( + db.gear, + { owner_id: 1 }, + { name: "gear_owned", partialFilterExpression: { "owned_versions.0": { $exists: true } } } +); // background_job: transient job-progress documents (TV Time import, reference-data "sync now") polled by // job id - MongoDB-backed (instead of in-memory) so any WebApi replica can answer a poll. TTL cleanup @@ -248,3 +270,8 @@ ensureIndex( { "external_ids.discogs": 1 }, { name: "album_reference_discogs_id", unique: true, partialFilterExpression: { "external_ids.discogs": { $exists: true } } } ); + +// user_preference: exactly one document per owner (upserted by owner_id, never listed) - the unique +// index is what actually guarantees that, the same way the application-level upsert-by-owner-id logic in +// UserPreferencesRepository is only "supposed to" prevent a second document. +ensureIndex(db.user_preference, { owner_id: 1 }, { name: "user_preference_owner", unique: true }); diff --git a/src/BlazorApp/Components/Account/Pages/Manage.razor b/src/BlazorApp/Components/Account/Pages/Manage.razor index fc74f1a0..162b95cf 100644 --- a/src/BlazorApp/Components/Account/Pages/Manage.razor +++ b/src/BlazorApp/Components/Account/Pages/Manage.razor @@ -1,5 +1,6 @@ -@page "/account/manage" +@page "/account/manage" @attribute [Authorize] +@inject UserPreferencesState PreferencesState

Manage

@@ -10,3 +11,38 @@

+ +@if (_preferences is not null) +{ +

Preferences

+
+ + +
+
+ + +
+} + +@code { + private UserPreferencesDto? _preferences; + + protected override async Task OnInitializedAsync() + { + _preferences = await PreferencesState.GetAsync(); + } + + private async Task SetFeatureAsync(Action apply, bool value) + { + if (_preferences is null) return; + apply(_preferences.Features, value); + await PreferencesState.UpdateAsync(_preferences); + } +} diff --git a/src/BlazorApp/Components/Account/UserPreferencesApiClient.cs b/src/BlazorApp/Components/Account/UserPreferencesApiClient.cs new file mode 100644 index 00000000..73fd5964 --- /dev/null +++ b/src/BlazorApp/Components/Account/UserPreferencesApiClient.cs @@ -0,0 +1,15 @@ +using Keeptrack.WebApi.Contracts.Dto; + +namespace Keeptrack.BlazorApp.Components.Account; + +public sealed class UserPreferencesApiClient(HttpClient http) +{ + public async Task GetAsync() + { + var result = await http.GetFromJsonAsync("/api/user-preferences"); + return result ?? new UserPreferencesDto(); + } + + public async Task UpdateAsync(UserPreferencesDto dto) => + (await http.PutAsJsonAsync("/api/user-preferences", dto)).EnsureSuccessStatusCode(); +} diff --git a/src/BlazorApp/Components/Account/UserPreferencesState.cs b/src/BlazorApp/Components/Account/UserPreferencesState.cs new file mode 100644 index 00000000..61db8cba --- /dev/null +++ b/src/BlazorApp/Components/Account/UserPreferencesState.cs @@ -0,0 +1,25 @@ +using Keeptrack.WebApi.Contracts.Dto; + +namespace Keeptrack.BlazorApp.Components.Account; + +/// +/// Circuit-scoped cache of the caller's own (registered AddScoped). +/// Preference checks now happen in more than one place per page (e.g. every owned copy's +/// OwnedVersionFields instance on a detail page), so each consumer fetching independently would fire +/// one HTTP request per instance; this fetches once per circuit and every consumer shares the same +/// in-flight/completed instead. (only called from +/// Manage.razor) refreshes the cache immediately, so a page navigated to afterward sees the new value +/// without a second round trip. +/// +public sealed class UserPreferencesState(UserPreferencesApiClient api) +{ + private Task? _cached; + + public Task GetAsync() => _cached ??= api.GetAsync(); + + public async Task UpdateAsync(UserPreferencesDto dto) + { + await api.UpdateAsync(dto); + _cached = Task.FromResult(dto); + } +} diff --git a/src/BlazorApp/Components/Import/AmazonImportApiClient.cs b/src/BlazorApp/Components/Import/AmazonImportApiClient.cs new file mode 100644 index 00000000..5571198c --- /dev/null +++ b/src/BlazorApp/Components/Import/AmazonImportApiClient.cs @@ -0,0 +1,28 @@ +using System.Net.Http.Headers; +using Keeptrack.WebApi.Contracts.Dto; + +namespace Keeptrack.BlazorApp.Components.Import; + +public sealed class AmazonImportApiClient(HttpClient http) +{ + public async Task> PreviewAsync(Stream csvStream, string fileName) + { + using var content = new MultipartFormDataContent(); + using var fileContent = new StreamContent(csvStream); + fileContent.Headers.ContentType = new MediaTypeHeaderValue("text/csv"); + content.Add(fileContent, "file", fileName); + + var response = await http.PostAsync("/api/import/amazon/preview", content); + response.EnsureSuccessStatusCode(); + + return (await response.Content.ReadFromJsonAsync>())!; + } + + public async Task CommitAsync(List items) + { + var response = await http.PostAsJsonAsync("/api/import/amazon/commit", new AmazonImportCommitRequestDto { Items = items }); + response.EnsureSuccessStatusCode(); + + return (await response.Content.ReadFromJsonAsync())!; + } +} diff --git a/src/BlazorApp/Components/Import/AmazonImportPage.razor b/src/BlazorApp/Components/Import/AmazonImportPage.razor new file mode 100644 index 00000000..f99d9b20 --- /dev/null +++ b/src/BlazorApp/Components/Import/AmazonImportPage.razor @@ -0,0 +1,314 @@ +@page "/import/amazon" +@attribute [Authorize] +@using Keeptrack.WebApi.Contracts.Dto +@using Keeptrack.BlazorApp.Components.Inventory.Pages + +
+

Import from Amazon

+
+ +
+

+ Upload the order-history CSV from Amazon's "Request My Data" export. + Amazon's export has no category column, so every row is shown for you to review and pick a type for. + Nothing is imported until you click "Import selected" below. +

+ +
+ + +
+ + @if (_previewing) + { +

Reading your export…

+ } + + @if (_error is not null) + { +
@_error
+ } +
+ +@if (_rows is not null) +{ +
+
+
@DisplayedRows.Count() row(s) shown
+
+ + +
+
+ +
+ + + + + + + + + + + + + + @foreach (var row in DisplayedRows) + { + + + + + + + + + + } + +
+ + TitleYearTypeCopyPlatformOrder info
+ + @if (row.Preview.AlreadyImported) + { + + } + + + + + + @if (row.MediaType == AmazonImportMediaType.VideoGame) + { + + } + else + { + + } + + @if (row.Isbn is not null) + { +
@row.Isbn
+ } + @if (row.AcquiredAt is not null) + { +
@row.AcquiredAt.Value.ToString("yyyy-MM-dd")
+ } + @if (row.Price is not null) + { +
@row.Price.Value.ToString("0.00") €
+ } +
+
+ + @if (SelectedRows.Any(r => r.MediaType is null)) + { +

Pick a type for every selected row before importing.

+ } + + @if (SelectedRows.Any(IsMissingPlatform)) + { +

Pick a platform for every selected video game before importing.

+ } + + +
+} + +@if (_commitError is not null) +{ +
@_commitError
+} + +@if (_commitResult is not null) +{ +
+

Books: @_commitResult.BooksCreated created, @_commitResult.BooksMergedInto merged into existing, @_commitResult.BooksSkipped already imported

+

Movies: @_commitResult.MoviesCreated created, @_commitResult.MoviesMergedInto merged into existing, @_commitResult.MoviesSkipped already imported

+

TV shows: @_commitResult.TvShowsCreated created, @_commitResult.TvShowsMergedInto merged into existing, @_commitResult.TvShowsSkipped already imported

+

Video games: @_commitResult.VideoGamesCreated created, @_commitResult.VideoGamesMergedInto merged into existing, @_commitResult.VideoGamesSkipped already imported

+

Gear: @_commitResult.GearCreated created, @_commitResult.GearMergedInto merged into existing, @_commitResult.GearSkipped already imported

+

Collectibles: @_commitResult.CollectiblesCreated created, @_commitResult.CollectiblesMergedInto merged into existing, @_commitResult.CollectiblesSkipped already imported

+
+} + +@code { + private const long MaxFileSize = 20_000_000; + + [Inject] private AmazonImportApiClient ImportApi { get; set; } = null!; + + private bool _previewing; + private string? _error; + private List? _rows; + private bool _showAll; + + /// Defaults on - a re-uploaded export is mostly already-imported rows, and hiding them by + /// default is what actually keeps the review list short; "Show all order items" above is a separate, + /// orthogonal filter (books vs. everything). + private bool _hideAlreadyImported = true; + + private bool _committing; + private string? _commitError; + private AmazonImportCommitResultDto? _commitResult; + + private IEnumerable DisplayedRows => (_rows ?? []) + .Where(r => _showAll || r.Preview.LooksLikeBook) + .Where(r => !_hideAlreadyImported || !r.Preview.AlreadyImported); + + private IEnumerable SelectedRows => _rows is null ? [] : _rows.Where(r => r.Selected); + + private static bool IsMissingPlatform(EditableRow row) => row.MediaType == AmazonImportMediaType.VideoGame && string.IsNullOrEmpty(row.Platform); + + /// Drives the header checkbox: checked only once every currently-shown row is selected. + private bool AllDisplayedSelected => DisplayedRows.Any() && DisplayedRows.All(r => r.Selected); + + /// Applies to whatever currently shows, not the whole underlying list - + /// e.g. selecting all while only likely-books are shown never touches the hidden non-book rows. + private void SetAllDisplayedSelected(bool selected) + { + foreach (var row in DisplayedRows) + { + row.Selected = selected; + } + } + + private async Task OnFileSelectedAsync(InputFileChangeEventArgs e) + { + _previewing = true; + _error = null; + _rows = null; + _commitResult = null; + _commitError = null; + + try + { +#pragma warning disable S5693 + await using var stream = e.File.OpenReadStream(MaxFileSize); +#pragma warning restore S5693 + var preview = await ImportApi.PreviewAsync(stream, e.File.Name); + _rows = preview.Select(ToEditableRow).ToList(); + } + catch (Exception ex) + { + _error = ex.Message; + } + finally + { + _previewing = false; + } + } + + private async Task CommitAsync() + { + _committing = true; + _commitError = null; + _commitResult = null; + + try + { + var items = SelectedRows.Select(row => new AmazonImportCommitItemDto + { + RowId = row.Preview.RowId, + // Echoed back rather than trusting a client-computed Reference string - the server derives + // the actual owned-copy Reference (and dedup key) from these two itself, which is what makes + // two different items sharing one Amazon order distinguishable from each other. + OrderId = row.Preview.OrderId, + Asin = row.Preview.Asin, + Title = row.Title, + // The original, unedited title from the parsed preview - kept separate from the editable + // row.Title above so it can be recorded verbatim in the created item's notes, regardless of + // whatever the user typed over it here. + AmazonTitle = row.Preview.Title, + MediaType = row.MediaType, + // Amazon's export has no publication year (only the purchase date), so it's never pre-filled - + // but worth entering by hand here if known, since it sharpens the deferred reference-lookup + // pass's match precision later (an ambiguous multi-candidate title+year search is left + // unresolved for manual review rather than guessed, same as everywhere else in the app). + Year = row.Year, + Isbn = row.Isbn, + // Only meaningful (and only validated server-side) for a VideoGame row. + Platform = row.MediaType == AmazonImportMediaType.VideoGame ? row.Platform : null, + AcquiredAt = row.AcquiredAt, + Price = row.Price, + Vendor = row.Vendor, + CopyType = row.CopyType + }).ToList(); + + _commitResult = await ImportApi.CommitAsync(items); + } + catch (Exception ex) + { + _commitError = ex.Message; + } + finally + { + _committing = false; + } + } + + private static EditableRow ToEditableRow(AmazonOrderPreviewRowDto preview) => new() + { + Preview = preview, + Selected = preview.LooksLikeBook && !preview.AlreadyImported, + // Empty (forcing an explicit choice) unless the ISBN heuristic suggested a book - there's no + // equivalent signal for the other three types, so guessing one of them would be worse than asking. + MediaType = preview.LooksLikeBook ? AmazonImportMediaType.Book : null, + Title = preview.Title, + Isbn = preview.SuggestedIsbn, + AcquiredAt = preview.OrderDate, + Price = preview.Price, + Vendor = preview.Vendor + }; + + private sealed class EditableRow + { + public required AmazonOrderPreviewRowDto Preview { get; init; } + public bool Selected { get; set; } + + /// The one field worth editing at import time - everything else below is read-only, order-derived info. + public string Title { get; set; } = ""; + + /// Never pre-filled (Amazon's export has no publication year) - worth entering by hand if known, see . + public int? Year { get; set; } + + /// + /// Null (forcing an explicit pick, see 's sibling commit-button + /// guard) unless the ISBN heuristic suggested Book - see . + /// + public AmazonImportMediaType? MediaType { get; set; } + + public string? Isbn { get; set; } + public DateOnly? AcquiredAt { get; set; } + public decimal? Price { get; set; } + public string? Vendor { get; set; } + + /// The other field worth a decision - Amazon's export can't reliably tell physical from digital. + public CopyType CopyType { get; set; } + + /// PS5/Xbox/PC/Switch... - only relevant (and only sent) when is VideoGame. + public string? Platform { get; set; } + } +} diff --git a/src/BlazorApp/Components/Import/GenericVideoGameImportApiClient.cs b/src/BlazorApp/Components/Import/GenericVideoGameImportApiClient.cs new file mode 100644 index 00000000..ef544612 --- /dev/null +++ b/src/BlazorApp/Components/Import/GenericVideoGameImportApiClient.cs @@ -0,0 +1,28 @@ +using System.Net.Http.Headers; +using Keeptrack.WebApi.Contracts.Dto; + +namespace Keeptrack.BlazorApp.Components.Import; + +public sealed class GenericVideoGameImportApiClient(HttpClient http) +{ + public async Task> PreviewAsync(Stream csvStream, string fileName) + { + using var content = new MultipartFormDataContent(); + using var fileContent = new StreamContent(csvStream); + fileContent.Headers.ContentType = new MediaTypeHeaderValue("text/csv"); + content.Add(fileContent, "file", fileName); + + var response = await http.PostAsync("/api/import/video-games/preview", content); + response.EnsureSuccessStatusCode(); + + return (await response.Content.ReadFromJsonAsync>())!; + } + + public async Task CommitAsync(List items) + { + var response = await http.PostAsJsonAsync("/api/import/video-games/commit", new GenericVideoGameImportCommitRequestDto { Items = items }); + response.EnsureSuccessStatusCode(); + + return (await response.Content.ReadFromJsonAsync())!; + } +} diff --git a/src/BlazorApp/Components/Import/GenericVideoGameImportPage.razor b/src/BlazorApp/Components/Import/GenericVideoGameImportPage.razor new file mode 100644 index 00000000..b2c0d39f --- /dev/null +++ b/src/BlazorApp/Components/Import/GenericVideoGameImportPage.razor @@ -0,0 +1,274 @@ +@page "/import/video-games" +@attribute [Authorize] +@using Keeptrack.WebApi.Contracts.Dto +@using Keeptrack.BlazorApp.Components.Inventory.Pages + +
+

Import video game transactions

+
+ +
+

+ Upload a video game transaction-history CSV with columns: + Transaction Date, Game Name, Product Name, Platform, Vendor, Transaction Id, Order Id, Final Price (€). + This isn't store-specific - it works for any export shaped this way (e.g. a PlayStation Store GDPR export). + Nothing is imported until you click "Import selected" below. +

+ +
+ + +
+ + @if (_previewing) + { +

Reading your export…

+ } + + @if (_error is not null) + { +
@_error
+ } +
+ +@if (_rows is not null) +{ +
+
+
@DisplayedRows.Count() row(s) shown
+ +
+ +
+ + + + + + + + + + + + + @foreach (var row in DisplayedRows) + { + + + + + + + + + } + +
+ + TitleYearCopyPlatformOrder info
+ + @if (row.Preview.AlreadyImported) + { + + } + + + + + + @if (row.Preview.ProductName is not null) + { +
@row.Preview.ProductName
+ } + @if (row.Preview.TransactionDate is not null) + { +
@row.Preview.TransactionDate.Value.ToString("yyyy-MM-dd")
+ } + @if (row.Preview.Price is not null) + { +
@row.Preview.Price.Value.ToString("0.00") €
+ } +
+
+ + @if (SelectedRows.Any(r => string.IsNullOrWhiteSpace(r.Platform))) + { +

Pick a platform for every selected row before importing.

+ } + + +
+} + +@if (_commitError is not null) +{ +
@_commitError
+} + +@if (_commitResult is not null) +{ +
+

Video games: @_commitResult.VideoGamesCreated created, @_commitResult.VideoGamesMergedInto merged into existing, @_commitResult.VideoGamesSkipped already imported

+

+ @_commitResult.RowsImported of @_lastCommitRowCount selected row(s) imported. + @if (_commitResult.VideoGamesCreated + _commitResult.VideoGamesMergedInto < _commitResult.RowsImported) + { + Several rows shared a title and were combined into the same video game, so the created/merged count above is lower than the row count - nothing was lost. + } +

+ @if (_commitResult.SkippedRowTitles.Count > 0) + { +
+

Not imported (already imported before):

+
    + @foreach (var title in _commitResult.SkippedRowTitles) + { +
  • @title
  • + } +
+ } +
+} + +@code { + private const long MaxFileSize = 20_000_000; + + [Inject] private GenericVideoGameImportApiClient ImportApi { get; set; } = null!; + + private bool _previewing; + private string? _error; + private List? _rows; + + /// Defaults on - a re-uploaded export is mostly already-imported rows, and hiding them by + /// default is what actually keeps the review list short. + private bool _hideAlreadyImported = true; + + private bool _committing; + private string? _commitError; + private GenericVideoGameImportCommitResultDto? _commitResult; + + /// How many rows the last commit actually submitted - the reconciliation total shown next to + /// . + private int _lastCommitRowCount; + + private IEnumerable DisplayedRows => (_rows ?? []) + .Where(r => !_hideAlreadyImported || !r.Preview.AlreadyImported); + + private IEnumerable SelectedRows => _rows is null ? [] : _rows.Where(r => r.Selected); + + /// Drives the header checkbox: checked only once every currently-shown row is selected. + private bool AllDisplayedSelected => DisplayedRows.Any() && DisplayedRows.All(r => r.Selected); + + private void SetAllDisplayedSelected(bool selected) + { + foreach (var row in DisplayedRows) + { + row.Selected = selected; + } + } + + private async Task OnFileSelectedAsync(InputFileChangeEventArgs e) + { + _previewing = true; + _error = null; + _rows = null; + _commitResult = null; + _commitError = null; + + try + { +#pragma warning disable S5693 + await using var stream = e.File.OpenReadStream(MaxFileSize); +#pragma warning restore S5693 + var preview = await ImportApi.PreviewAsync(stream, e.File.Name); + _rows = preview.Select(ToEditableRow).ToList(); + } + catch (Exception ex) + { + _error = ex.Message; + } + finally + { + _previewing = false; + } + } + + private async Task CommitAsync() + { + _committing = true; + _commitError = null; + _commitResult = null; + + try + { + var items = SelectedRows.Select(row => new GenericVideoGameImportCommitItemDto + { + RowId = row.Preview.RowId, + Title = row.Title, + // The original, unedited title from the parsed preview - kept separate from the editable + // row.Title above so it can be recorded verbatim in the created item's notes, regardless of + // whatever the user typed over it here. + SourceTitle = row.Preview.Title, + Platform = row.Platform, + ProductName = row.Preview.ProductName, + TransactionId = row.Preview.TransactionId, + OrderId = row.Preview.OrderId, + Vendor = row.Preview.Vendor, + Year = row.Year, + AcquiredAt = row.Preview.TransactionDate, + Price = row.Preview.Price, + CopyType = row.CopyType + }).ToList(); + + _lastCommitRowCount = items.Count; + _commitResult = await ImportApi.CommitAsync(items); + } + catch (Exception ex) + { + _commitError = ex.Message; + } + finally + { + _committing = false; + } + } + + private static EditableRow ToEditableRow(GenericVideoGameImportPreviewRowDto preview) => new() + { + Preview = preview, + Selected = !preview.AlreadyImported, + Title = preview.Title, + Platform = preview.Platform, + // This export is store purchase history, so a copy is inherently digital - still editable in case + // of an oddity (e.g. a listed physical bundle/voucher line). + CopyType = CopyType.Digital + }; + + private sealed class EditableRow + { + public required GenericVideoGameImportPreviewRowDto Preview { get; init; } + public bool Selected { get; set; } + + /// The one field worth editing at import time besides Year/Platform/Copy - everything else is read-only, order-derived info. + public string Title { get; set; } = ""; + + /// Never pre-filled - the export has no publication/release year. + public int? Year { get; set; } + + public string? Platform { get; set; } + + public CopyType CopyType { get; set; } + } +} diff --git a/src/BlazorApp/Components/Import/ImportPage.razor b/src/BlazorApp/Components/Import/ImportPage.razor index 6f5830b2..d7d77a17 100644 --- a/src/BlazorApp/Components/Import/ImportPage.razor +++ b/src/BlazorApp/Components/Import/ImportPage.razor @@ -2,6 +2,28 @@ @attribute [Authorize]
+

Import from Amazon

+
+ +
+

+ Review your Amazon order history and pick which items to import as books (only books are supported so far). +

+ Import from Amazon +
+ +
+

Import video game transactions

+
+ +
+

+ Review a video game store transaction history (e.g. a PlayStation Store GDPR export) and pick which items to import. +

+ Import video game transactions +
+ +

Import from TV Time

diff --git a/src/BlazorApp/Components/Inventory/Clients/AlbumApiClient.cs b/src/BlazorApp/Components/Inventory/Clients/AlbumApiClient.cs index c86c5b12..977bb18b 100644 --- a/src/BlazorApp/Components/Inventory/Clients/AlbumApiClient.cs +++ b/src/BlazorApp/Components/Inventory/Clients/AlbumApiClient.cs @@ -13,4 +13,15 @@ public async Task RefreshReferenceAsync(string id) response.EnsureSuccessStatusCode(); return (await response.Content.ReadFromJsonAsync())!; } + + /// + /// Admin-only: unlinks and permanently deletes the shared reference document + /// (POST api/albums/{id}/unlink-reference on WebApi). + /// + public async Task UnlinkReferenceAsync(string id) + { + var response = await Http.PostAsync($"{ApiResourceName}/{id}/unlink-reference", null); + response.EnsureSuccessStatusCode(); + return (await response.Content.ReadFromJsonAsync())!; + } } diff --git a/src/BlazorApp/Components/Inventory/Clients/BookApiClient.cs b/src/BlazorApp/Components/Inventory/Clients/BookApiClient.cs index 6d9a0b45..9feb451c 100644 --- a/src/BlazorApp/Components/Inventory/Clients/BookApiClient.cs +++ b/src/BlazorApp/Components/Inventory/Clients/BookApiClient.cs @@ -13,4 +13,15 @@ public async Task RefreshReferenceAsync(string id) response.EnsureSuccessStatusCode(); return (await response.Content.ReadFromJsonAsync())!; } + + /// + /// Admin-only: unlinks and permanently deletes the shared reference document + /// (POST api/books/{id}/unlink-reference on WebApi). + /// + public async Task UnlinkReferenceAsync(string id) + { + var response = await Http.PostAsync($"{ApiResourceName}/{id}/unlink-reference", null); + response.EnsureSuccessStatusCode(); + return (await response.Content.ReadFromJsonAsync())!; + } } diff --git a/src/BlazorApp/Components/Inventory/Clients/CollectibleApiClient.cs b/src/BlazorApp/Components/Inventory/Clients/CollectibleApiClient.cs new file mode 100644 index 00000000..30a1d6fc --- /dev/null +++ b/src/BlazorApp/Components/Inventory/Clients/CollectibleApiClient.cs @@ -0,0 +1,9 @@ +using Keeptrack.WebApi.Contracts.Dto; + +namespace Keeptrack.BlazorApp.Components.Inventory.Clients; + +public sealed class CollectibleApiClient(HttpClient http) + : InventoryApiClientBase(http) +{ + protected override string ApiResourceName => "/api/collectibles"; +} diff --git a/src/BlazorApp/Components/Inventory/Clients/GearApiClient.cs b/src/BlazorApp/Components/Inventory/Clients/GearApiClient.cs new file mode 100644 index 00000000..0abdeeba --- /dev/null +++ b/src/BlazorApp/Components/Inventory/Clients/GearApiClient.cs @@ -0,0 +1,13 @@ +using Keeptrack.WebApi.Contracts.Dto; + +namespace Keeptrack.BlazorApp.Components.Inventory.Clients; + +public sealed class GearApiClient(HttpClient http) + : InventoryApiClientBase(http) +{ + protected override string ApiResourceName => "/api/gear"; + + /// Distinct categories already used across this tenant's gear - see GearController.GetCategories. + public async Task> GetCategoriesAsync() => + await Http.GetFromJsonAsync>($"{ApiResourceName}/categories") ?? []; +} diff --git a/src/BlazorApp/Components/Inventory/Clients/InventoryApiClientBase.cs b/src/BlazorApp/Components/Inventory/Clients/InventoryApiClientBase.cs index b0f30496..78b869eb 100644 --- a/src/BlazorApp/Components/Inventory/Clients/InventoryApiClientBase.cs +++ b/src/BlazorApp/Components/Inventory/Clients/InventoryApiClientBase.cs @@ -42,7 +42,7 @@ public async Task AddAsync(TDto movie) var response = await http.PostAsJsonAsync($"{ApiResourceName}", movie); if (!response.IsSuccessStatusCode) { - // the API returns error bodies (free-tier quota 403s, ApiExceptionFilterAttribute's 400s/500s); + // the API returns error bodies (free-tier quota 403s, ApiExceptionFilterAttribute's 400s/500s) - // surfacing that text beats EnsureSuccessStatusCode's opaque "403 (Forbidden)" in the Add form var body = await response.Content.ReadFromJsonAsync(); throw new InvalidOperationException(string.IsNullOrEmpty(body?.Error) diff --git a/src/BlazorApp/Components/Inventory/Clients/MovieApiClient.cs b/src/BlazorApp/Components/Inventory/Clients/MovieApiClient.cs index 66c9540f..1cfa0983 100644 --- a/src/BlazorApp/Components/Inventory/Clients/MovieApiClient.cs +++ b/src/BlazorApp/Components/Inventory/Clients/MovieApiClient.cs @@ -18,4 +18,15 @@ public async Task RefreshReferenceAsync(string id) response.EnsureSuccessStatusCode(); return (await response.Content.ReadFromJsonAsync())!; } + + /// + /// Admin-only: unlinks and permanently deletes the shared reference document + /// (POST api/movies/{id}/unlink-reference on WebApi). + /// + public async Task UnlinkReferenceAsync(string id) + { + var response = await Http.PostAsync($"{ApiResourceName}/{id}/unlink-reference", null); + response.EnsureSuccessStatusCode(); + return (await response.Content.ReadFromJsonAsync())!; + } } diff --git a/src/BlazorApp/Components/Inventory/Clients/TvShowApiClient.cs b/src/BlazorApp/Components/Inventory/Clients/TvShowApiClient.cs index 6458d356..8bd2ad4e 100644 --- a/src/BlazorApp/Components/Inventory/Clients/TvShowApiClient.cs +++ b/src/BlazorApp/Components/Inventory/Clients/TvShowApiClient.cs @@ -18,4 +18,15 @@ public async Task RefreshReferenceAsync(string id) response.EnsureSuccessStatusCode(); return (await response.Content.ReadFromJsonAsync())!; } + + /// + /// Admin-only: unlinks and permanently deletes the shared reference document + /// (POST api/tv-shows/{id}/unlink-reference on WebApi). + /// + public async Task UnlinkReferenceAsync(string id) + { + var response = await Http.PostAsync($"{ApiResourceName}/{id}/unlink-reference", null); + response.EnsureSuccessStatusCode(); + return (await response.Content.ReadFromJsonAsync())!; + } } diff --git a/src/BlazorApp/Components/Inventory/Clients/VideoGameApiClient.cs b/src/BlazorApp/Components/Inventory/Clients/VideoGameApiClient.cs index d5e64d3b..45df7e83 100644 --- a/src/BlazorApp/Components/Inventory/Clients/VideoGameApiClient.cs +++ b/src/BlazorApp/Components/Inventory/Clients/VideoGameApiClient.cs @@ -13,4 +13,15 @@ public async Task RefreshReferenceAsync(string id) response.EnsureSuccessStatusCode(); return (await response.Content.ReadFromJsonAsync())!; } + + /// + /// Admin-only: unlinks and permanently deletes the shared reference document + /// (POST api/video-games/{id}/unlink-reference on WebApi). + /// + public async Task UnlinkReferenceAsync(string id) + { + var response = await Http.PostAsync($"{ApiResourceName}/{id}/unlink-reference", null); + response.EnsureSuccessStatusCode(); + return (await response.Content.ReadFromJsonAsync())!; + } } diff --git a/src/BlazorApp/Components/Inventory/Pages/AlbumDetail.razor b/src/BlazorApp/Components/Inventory/Pages/AlbumDetail.razor index 160159e7..901a887d 100644 --- a/src/BlazorApp/Components/Inventory/Pages/AlbumDetail.razor +++ b/src/BlazorApp/Components/Inventory/Pages/AlbumDetail.razor @@ -31,6 +31,17 @@ else title="@(string.IsNullOrEmpty(Album.ReferenceId) ? "Check for reference match using the title and year below." : $"Linked to \"{Reference?.Title}\". Not right? Edit the title or year below, then check again.")"> + @if (!string.IsNullOrEmpty(Album.ReferenceId)) + { + + + + + + } @if (_refreshMessage is not null) { @_refreshMessage @@ -49,11 +60,18 @@ else } + +
-
- @if (!string.IsNullOrEmpty(Reference?.ImageUrl)) + @{ var coverUrl = !string.IsNullOrEmpty(Album.CustomImageUrl) ? Album.CustomImageUrl : Reference?.ImageUrl; } +
+ @if (!string.IsNullOrEmpty(coverUrl)) { - @Album.Title cover + @Album.Title cover }
@@ -74,6 +92,11 @@ else
+
+ + +
@@ -161,6 +184,7 @@ else private string? _refreshMessage; private string _refreshMessageStyle = "neutral"; private object? _refreshMessageToken; + private bool _pendingUnlink; /// Reuses CastGrid's "photo + name" card for the single credited artist, instead of /// introducing a one-off component for what's visually the same layout as a TV/movie cast member. @@ -251,9 +275,7 @@ else private async Task SaveArtistAsync(ChangeEventArgs e) { if (Album is null) return; - var artist = e.Value?.ToString(); - if (string.IsNullOrWhiteSpace(artist)) return; - Album.Artist = artist; + Album.Artist = e.Value?.ToString(); await AlbumApi.UpdateAsync(Album); } @@ -264,6 +286,13 @@ else await AlbumApi.UpdateAsync(Album); } + private async Task SaveCustomImageUrlAsync(ChangeEventArgs e) + { + if (Album is null) return; + Album.CustomImageUrl = e.Value?.ToString(); + await AlbumApi.UpdateAsync(Album); + } + private async Task RefreshReferenceAsync() { if (Album?.Id is null) return; @@ -276,9 +305,7 @@ else var refreshed = await AlbumApi.RefreshReferenceAsync(Album.Id); await LoadAsync(); - (_refreshMessage, _refreshMessageStyle) = string.IsNullOrEmpty(refreshed.ReferenceId) - ? string.IsNullOrEmpty(previousReferenceId) ? ("No match found", "neutral") : ("Unlinked - no match", "danger") - : refreshed.ReferenceId == previousReferenceId ? ("Already linked", "neutral") : ("Linked!", "success"); + (_refreshMessage, _refreshMessageStyle) = ReferenceRefreshMessage.Compute(previousReferenceId, refreshed.ReferenceId); } catch (Exception ex) { @@ -293,6 +320,26 @@ else ScheduleRefreshMessageClear(); } + private async Task UnlinkReferenceAsync() + { + _pendingUnlink = false; + if (Album?.Id is null) return; + + try + { + await AlbumApi.UnlinkReferenceAsync(Album.Id); + await LoadAsync(); + (_refreshMessage, _refreshMessageStyle) = ("Unlinked and reference removed.", "success"); + } + catch (Exception ex) + { + _refreshMessage = $"Error: {ex.Message}"; + _refreshMessageStyle = "danger"; + } + + ScheduleRefreshMessageClear(); + } + /// /// The toast is a small, auto-dismissing pill next to the refresh icon, not a permanent line of text - /// a token guards against an in-flight delay from a previous click clearing a newer message. diff --git a/src/BlazorApp/Components/Inventory/Pages/BookDetail.razor b/src/BlazorApp/Components/Inventory/Pages/BookDetail.razor index d915ccbf..c1fb7cb3 100644 --- a/src/BlazorApp/Components/Inventory/Pages/BookDetail.razor +++ b/src/BlazorApp/Components/Inventory/Pages/BookDetail.razor @@ -31,6 +31,17 @@ else title="@(string.IsNullOrEmpty(Book.ReferenceId) ? "Check for reference match using the title and year below." : $"Linked to \"{Reference?.Title}\". Not right? Edit the title or year below, then check again.")"> + @if (!string.IsNullOrEmpty(Book.ReferenceId)) + { + + + + + + } @if (_refreshMessage is not null) { @_refreshMessage @@ -50,6 +61,12 @@ else } + +
@(Book.FirstReadAt is not null ? "✓ Read" : "Mark as read") @@ -98,7 +115,16 @@ else
- +
+ + @if (!string.IsNullOrEmpty(Book.Isbn) && ShowChasseAuxLivresLink) + { + + + + } +
@@ -131,6 +157,8 @@ else [Inject] private ReferenceDataApiClient ReferenceDataApi { get; set; } = null!; + [Inject] private UserPreferencesState PreferencesState { get; set; } = null!; + private static readonly TimeSpan RefreshMessageDuration = TimeSpan.FromSeconds(4); // _loading is delay-gated (see LoadingIndicator) and only turns on for a load that's genuinely slow. @@ -149,10 +177,14 @@ else [PersistentState] public BookReferenceDto? Reference { get; set; } + + [PersistentState] + public bool ShowChasseAuxLivresLink { get; set; } private bool _refreshingReference; private string? _refreshMessage; private string _refreshMessageStyle = "neutral"; private object? _refreshMessageToken; + private bool _pendingUnlink; protected override async Task OnParametersSetAsync() { @@ -178,6 +210,7 @@ else { Book = await BookApi.GetOneAsync(Id); Reference = string.IsNullOrEmpty(Book?.ReferenceId) ? null : await ReferenceDataApi.GetBookAsync(Book.ReferenceId); + ShowChasseAuxLivresLink = (await PreferencesState.GetAsync()).Features.ShowChasseAuxLivresLink; } private async Task SetRatingAsync(float rating) @@ -217,7 +250,7 @@ else private async Task SetReadDateAsync(ChangeEventArgs e) { if (Book is null) return; - Book.FirstReadAt = DateOnly.TryParse(e.Value?.ToString(), out var date) ? date : null; + Book.FirstReadAt = DateOnlyInput.Parse(e.Value); await BookApi.UpdateAsync(Book); } @@ -233,9 +266,7 @@ else private async Task SaveAuthorAsync(ChangeEventArgs e) { if (Book is null) return; - var author = e.Value?.ToString(); - if (string.IsNullOrWhiteSpace(author)) return; - Book.Author = author; + Book.Author = e.Value?.ToString(); await BookApi.UpdateAsync(Book); } @@ -286,9 +317,7 @@ else var refreshed = await BookApi.RefreshReferenceAsync(Book.Id); await LoadAsync(); - (_refreshMessage, _refreshMessageStyle) = string.IsNullOrEmpty(refreshed.ReferenceId) - ? string.IsNullOrEmpty(previousReferenceId) ? ("No match found", "neutral") : ("Unlinked - no match", "danger") - : refreshed.ReferenceId == previousReferenceId ? ("Already linked", "neutral") : ("Linked!", "success"); + (_refreshMessage, _refreshMessageStyle) = ReferenceRefreshMessage.Compute(previousReferenceId, refreshed.ReferenceId); } catch (Exception ex) { @@ -303,6 +332,26 @@ else ScheduleRefreshMessageClear(); } + private async Task UnlinkReferenceAsync() + { + _pendingUnlink = false; + if (Book?.Id is null) return; + + try + { + await BookApi.UnlinkReferenceAsync(Book.Id); + await LoadAsync(); + (_refreshMessage, _refreshMessageStyle) = ("Unlinked and reference removed.", "success"); + } + catch (Exception ex) + { + _refreshMessage = $"Error: {ex.Message}"; + _refreshMessageStyle = "danger"; + } + + ScheduleRefreshMessageClear(); + } + /// /// The toast is a small, auto-dismissing pill next to the refresh icon, not a permanent line of text - /// a token guards against an in-flight delay from a previous click clearing a newer message. diff --git a/src/BlazorApp/Components/Inventory/Pages/CarDetail.razor b/src/BlazorApp/Components/Inventory/Pages/CarDetail.razor index d5198b1f..e55e6451 100644 --- a/src/BlazorApp/Components/Inventory/Pages/CarDetail.razor +++ b/src/BlazorApp/Components/Inventory/Pages/CarDetail.razor @@ -28,6 +28,13 @@ else
+ @if (!string.IsNullOrEmpty(Car.ImageUrl)) + { +
+ @Car.Name cover +
+ } +
@@ -57,6 +64,10 @@ else }
+
+ + +
@if (HasMetricsToShow) @@ -362,6 +373,13 @@ else await SaveCarAsync(); } + private async Task SaveImageUrlAsync(ChangeEventArgs e) + { + if (Car is null) return; + Car.ImageUrl = e.Value?.ToString(); + await SaveCarAsync(); + } + private void ShowAddModal() { _modalEntry = CarHistoryForm.NewEntry(Id); diff --git a/src/BlazorApp/Components/Inventory/Pages/Cars.razor b/src/BlazorApp/Components/Inventory/Pages/Cars.razor index 74737f4e..ae982c61 100644 --- a/src/BlazorApp/Components/Inventory/Pages/Cars.razor +++ b/src/BlazorApp/Components/Inventory/Pages/Cars.razor @@ -17,6 +17,8 @@ ShowForm="@_showForm" Form="@_form" ItemTitle="@(car => car.Name)" + ItemImageUrl="@(car => car.ImageUrl)" + ItemImageShape="wide" OnSearchKeyUp="@OnSearchKeyUp" OnClearSearch="@ClearSearch" OnGoToPage="@GoToPage" diff --git a/src/BlazorApp/Components/Inventory/Pages/CollectibleDetail.razor b/src/BlazorApp/Components/Inventory/Pages/CollectibleDetail.razor new file mode 100644 index 00000000..708d0634 --- /dev/null +++ b/src/BlazorApp/Components/Inventory/Pages/CollectibleDetail.razor @@ -0,0 +1,158 @@ +@page "/collectibles/{Id}" +@attribute [Authorize] + +@if (!_loaded) +{ + @if (_loading) + { +
+
+ Loading… +
+ } +} +else if (Collectible is null) +{ +
+
+

Collectible not found.

+
+} +else +{ + + +
+
+ +
+
+ +
+
+ + @if (!string.IsNullOrEmpty(Collectible.ImageUrl)) + { +
+ @Collectible.Title cover +
+ } + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +} + +@code { + [Parameter] public required string Id { get; set; } + + [Inject] private CollectibleApiClient CollectibleApi { get; set; } = null!; + + // _loading is delay-gated (see LoadingIndicator) and only turns on for a load that's genuinely slow. + // _loaded tracks whether a load attempt has finished at all, fresh or restored from persisted + // prerender state - both default false so the forced render Blazor triggers right after the + // synchronous prefix of OnParametersSetAsync (before any awaited fetch resolves) shows blank instead + // of a spinner flash on every navigation to this page. + private bool _loading; + private bool _loaded; + + // Public property (a framework requirement for [PersistentState]): the data loaded during the + // prerender pass is carried over to the interactive circuit, so the first interactive render reuses + // it instead of resetting to the spinner and re-fetching - same pattern as AlbumDetail/CarDetail. + [PersistentState] + public CollectibleDto? Collectible { get; set; } + + protected override async Task OnParametersSetAsync() + { + // Collectible already holds this route's item when PersistentState restored the prerendered data + // the id check keeps an in-circuit navigation to a different collectible reloading as before + if (Collectible?.Id == Id) + { + _loading = false; + _loaded = true; + return; + } + await LoadAsync(); + } + + private async Task LoadAsync() + { + await LoadingIndicator.RunAsync(FetchAsync(), v => _loading = v, StateHasChanged); + _loading = false; + _loaded = true; + } + + private async Task FetchAsync() + { + Collectible = await CollectibleApi.GetOneAsync(Id); + } + + private async Task SaveCollectibleAsync() + { + if (Collectible is null) return; + await CollectibleApi.UpdateAsync(Collectible); + } + + private async Task SaveTitleAsync(ChangeEventArgs e) + { + if (Collectible is null) return; + var title = e.Value?.ToString(); + if (string.IsNullOrWhiteSpace(title)) return; + Collectible.Title = title; + await SaveCollectibleAsync(); + } + + private async Task SaveBrandAsync(ChangeEventArgs e) + { + if (Collectible is null) return; + Collectible.Brand = e.Value?.ToString(); + await SaveCollectibleAsync(); + } + + private async Task SaveYearAsync(ChangeEventArgs e) + { + if (Collectible is null) return; + Collectible.Year = int.TryParse(e.Value?.ToString(), out var year) ? year : null; + await SaveCollectibleAsync(); + } + + private async Task SaveNotesAsync(ChangeEventArgs e) + { + if (Collectible is null) return; + Collectible.Notes = e.Value?.ToString(); + await SaveCollectibleAsync(); + } + + private async Task SaveImageUrlAsync(ChangeEventArgs e) + { + if (Collectible is null) return; + Collectible.ImageUrl = e.Value?.ToString(); + await SaveCollectibleAsync(); + } + + private async Task ToggleFavoriteAsync() + { + if (Collectible is null) return; + Collectible.IsFavorite = !Collectible.IsFavorite; + await SaveCollectibleAsync(); + } +} diff --git a/src/BlazorApp/Components/Inventory/Pages/Collectibles.razor b/src/BlazorApp/Components/Inventory/Pages/Collectibles.razor new file mode 100644 index 00000000..b4377ce1 --- /dev/null +++ b/src/BlazorApp/Components/Inventory/Pages/Collectibles.razor @@ -0,0 +1,65 @@ +@page "/collectibles" +@inherits InventoryPageBase +@attribute [Authorize] + + + + + + + + @if (!string.IsNullOrEmpty(collectible.Brand)) + { + @collectible.Brand + } + @if (collectible.Year > 0) + { + @collectible.Year + } + @if (collectible.IsFavorite) + { + Favorite + } + @if (collectible.OwnedVersions.Count > 0) + { + Owned + } + + +
+ +
+
+ +
+
+ +
+
+
diff --git a/src/BlazorApp/Components/Inventory/Pages/Collectibles.razor.cs b/src/BlazorApp/Components/Inventory/Pages/Collectibles.razor.cs new file mode 100644 index 00000000..3d0c11fa --- /dev/null +++ b/src/BlazorApp/Components/Inventory/Pages/Collectibles.razor.cs @@ -0,0 +1,30 @@ +using Keeptrack.WebApi.Contracts.Dto; +using Microsoft.AspNetCore.Components; + +namespace Keeptrack.BlazorApp.Components.Inventory.Pages; + +public partial class Collectibles : InventoryPageBase +{ + [Inject] private CollectibleApiClient CollectibleApi { get; set; } = null!; + + protected override InventoryApiClientBase Api => CollectibleApi; + + protected override string ListRoute => "/collectibles"; + + [SupplyParameterFromQuery(Name = "favorite")] + public bool FavoriteFilter { get; set; } + + [SupplyParameterFromQuery(Name = "owned")] + public bool OwnedFilter { get; set; } + + protected override IReadOnlyDictionary? ExtraQuery + { + get + { + var query = new Dictionary(); + if (FavoriteFilter) query["IsFavorite"] = "true"; + if (OwnedFilter) query["IsOwned"] = "true"; + return query.Count > 0 ? query : null; + } + } +} diff --git a/src/BlazorApp/Components/Inventory/Pages/Gear.razor b/src/BlazorApp/Components/Inventory/Pages/Gear.razor new file mode 100644 index 00000000..b43c1e2c --- /dev/null +++ b/src/BlazorApp/Components/Inventory/Pages/Gear.razor @@ -0,0 +1,80 @@ +@page "/gear" +@inherits InventoryPageBase +@attribute [Authorize] + + + + + + @if (Categories.Count > 0) + { + @* a button per category (VideoGame's State pattern) doesn't scale here - Category is an open, + tenant-grown list rather than a small fixed set. A plain + +
+ +
+
+ +
+ +
diff --git a/src/BlazorApp/Components/Inventory/Pages/Gear.razor.cs b/src/BlazorApp/Components/Inventory/Pages/Gear.razor.cs new file mode 100644 index 00000000..55e4bf4c --- /dev/null +++ b/src/BlazorApp/Components/Inventory/Pages/Gear.razor.cs @@ -0,0 +1,44 @@ +using Keeptrack.WebApi.Contracts.Dto; +using Microsoft.AspNetCore.Components; + +namespace Keeptrack.BlazorApp.Components.Inventory.Pages; + +public partial class Gear : InventoryPageBase +{ + [Inject] private GearApiClient GearApi { get; set; } = null!; + + protected override InventoryApiClientBase Api => GearApi; + + protected override string ListRoute => "/gear"; + + [SupplyParameterFromQuery(Name = "favorite")] + public bool FavoriteFilter { get; set; } + + [SupplyParameterFromQuery(Name = "owned")] + public bool OwnedFilter { get; set; } + + [SupplyParameterFromQuery(Name = "category")] + public string? CategoryFilter { get; set; } + + /// Distinct categories across this tenant's gear, for the Filters row - fetched once, not + /// tied to search/page/sort state like is. + protected List Categories { get; private set; } = []; + + protected override IReadOnlyDictionary? ExtraQuery + { + get + { + var query = new Dictionary(); + if (FavoriteFilter) query["IsFavorite"] = "true"; + if (OwnedFilter) query["IsOwned"] = "true"; + if (!string.IsNullOrEmpty(CategoryFilter)) query["Category"] = CategoryFilter; + return query.Count > 0 ? query : null; + } + } + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + Categories = await GearApi.GetCategoriesAsync(); + } +} diff --git a/src/BlazorApp/Components/Inventory/Pages/GearDetail.razor b/src/BlazorApp/Components/Inventory/Pages/GearDetail.razor new file mode 100644 index 00000000..db749ac3 --- /dev/null +++ b/src/BlazorApp/Components/Inventory/Pages/GearDetail.razor @@ -0,0 +1,179 @@ +@page "/gear/{Id}" +@attribute [Authorize] + +@if (!_loaded) +{ + @if (_loading) + { +
+
+ Loading… +
+ } +} +else if (Item is null) +{ +
+
+

Gear not found.

+
+} +else +{ + + +
+
+ +
+
+ +
+
+ + @if (!string.IsNullOrEmpty(Item.ImageUrl)) + { +
+ @Item.Title cover +
+ } + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +} + +@code { + [Parameter] public required string Id { get; set; } + + [Inject] private GearApiClient GearApi { get; set; } = null!; + + // _loading is delay-gated (see LoadingIndicator) and only turns on for a load that's genuinely slow. + // _loaded tracks whether a load attempt has finished at all, fresh or restored from persisted + // prerender state - both default false so the forced render Blazor triggers right after the + // synchronous prefix of OnParametersSetAsync (before any awaited fetch resolves) shows blank instead + // of a spinner flash on every navigation to this page. + private bool _loading; + private bool _loaded; + + // Suggested values for the Category input's datalist - fetched once, not tied to Item/Id like the + // rest of this page's state (a category typed on this item shouldn't wait on a second round trip). + private List _categories = []; + + // Public property (a framework requirement for [PersistentState]): the data loaded during the + // prerender pass is carried over to the interactive circuit, so the first interactive render reuses + // it instead of resetting to the spinner and re-fetching - same pattern as AlbumDetail/CarDetail. + // Named "Item" rather than "Gear" - a property can't share its containing partial class's own name. + [PersistentState] + public GearDto? Item { get; set; } + + protected override async Task OnInitializedAsync() + { + _categories = await GearApi.GetCategoriesAsync(); + } + + protected override async Task OnParametersSetAsync() + { + // Item already holds this route's item when PersistentState restored the prerendered data + // the id check keeps an in-circuit navigation to a different item reloading as before + if (Item?.Id == Id) + { + _loading = false; + _loaded = true; + return; + } + await LoadAsync(); + } + + private async Task LoadAsync() + { + await LoadingIndicator.RunAsync(FetchAsync(), v => _loading = v, StateHasChanged); + _loading = false; + _loaded = true; + } + + private async Task FetchAsync() + { + Item = await GearApi.GetOneAsync(Id); + } + + private async Task SaveItemAsync() + { + if (Item is null) return; + await GearApi.UpdateAsync(Item); + } + + private async Task SaveTitleAsync(ChangeEventArgs e) + { + if (Item is null) return; + var title = e.Value?.ToString(); + if (string.IsNullOrWhiteSpace(title)) return; + Item.Title = title; + await SaveItemAsync(); + } + + private async Task SaveBrandAsync(ChangeEventArgs e) + { + if (Item is null) return; + Item.Brand = e.Value?.ToString(); + await SaveItemAsync(); + } + + private async Task SaveYearAsync(ChangeEventArgs e) + { + if (Item is null) return; + Item.Year = int.TryParse(e.Value?.ToString(), out var year) ? year : null; + await SaveItemAsync(); + } + + private async Task SaveCategoryAsync(string? category) + { + if (Item is null) return; + Item.Category = category; + await SaveItemAsync(); + } + + private async Task SaveNotesAsync(ChangeEventArgs e) + { + if (Item is null) return; + Item.Notes = e.Value?.ToString(); + await SaveItemAsync(); + } + + private async Task SaveImageUrlAsync(ChangeEventArgs e) + { + if (Item is null) return; + Item.ImageUrl = e.Value?.ToString(); + await SaveItemAsync(); + } + + private async Task ToggleFavoriteAsync() + { + if (Item is null) return; + Item.IsFavorite = !Item.IsFavorite; + await SaveItemAsync(); + } +} diff --git a/src/BlazorApp/Components/Inventory/Pages/HealthProfileDetail.razor b/src/BlazorApp/Components/Inventory/Pages/HealthProfileDetail.razor index 106b4253..99cc1f97 100644 --- a/src/BlazorApp/Components/Inventory/Pages/HealthProfileDetail.razor +++ b/src/BlazorApp/Components/Inventory/Pages/HealthProfileDetail.razor @@ -28,9 +28,24 @@ else + @if (!string.IsNullOrEmpty(Profile.ImageUrl)) + { +
+ @Profile.Name cover +
+ } +
- - +
+
+ + +
+
+ + +
+
@* No summary panels before the journal on purpose (owner feedback): @@ -258,6 +273,13 @@ else await SaveProfileAsync(); } + private async Task SaveImageUrlAsync(ChangeEventArgs e) + { + if (Profile is null) return; + Profile.ImageUrl = e.Value?.ToString(); + await SaveProfileAsync(); + } + private void ShowAddModal() { _modalEntry = HealthRecordForm.NewEntry(Id); diff --git a/src/BlazorApp/Components/Inventory/Pages/HealthProfiles.razor b/src/BlazorApp/Components/Inventory/Pages/HealthProfiles.razor index cabc274a..c2616c43 100644 --- a/src/BlazorApp/Components/Inventory/Pages/HealthProfiles.razor +++ b/src/BlazorApp/Components/Inventory/Pages/HealthProfiles.razor @@ -17,6 +17,8 @@ ShowForm="@_showForm" Form="@_form" ItemTitle="@(profile => profile.Name)" + ItemImageUrl="@(profile => profile.ImageUrl)" + ItemImageShape="wide" OnSearchKeyUp="@OnSearchKeyUp" OnClearSearch="@ClearSearch" OnGoToPage="@GoToPage" diff --git a/src/BlazorApp/Components/Inventory/Pages/HouseDetail.razor b/src/BlazorApp/Components/Inventory/Pages/HouseDetail.razor index 2f055de0..a9ae3206 100644 --- a/src/BlazorApp/Components/Inventory/Pages/HouseDetail.razor +++ b/src/BlazorApp/Components/Inventory/Pages/HouseDetail.razor @@ -35,6 +35,13 @@ else } + @if (!string.IsNullOrEmpty(House.ImageUrl)) + { +
+ @House.Name cover +
+ } +
@@ -53,6 +60,10 @@ else
+
+ + +
@@ -306,14 +317,14 @@ else private async Task SaveMovedInAtAsync(ChangeEventArgs e) { if (House is null) return; - House.MovedInAt = DateOnly.TryParse(e.Value?.ToString(), out var date) ? date : null; + House.MovedInAt = DateOnlyInput.Parse(e.Value); await SaveHouseAsync(); } private async Task SaveMovedOutAtAsync(ChangeEventArgs e) { if (House is null) return; - House.MovedOutAt = DateOnly.TryParse(e.Value?.ToString(), out var date) ? date : null; + House.MovedOutAt = DateOnlyInput.Parse(e.Value); await SaveHouseAsync(); } @@ -324,6 +335,13 @@ else await SaveHouseAsync(); } + private async Task SaveImageUrlAsync(ChangeEventArgs e) + { + if (House is null) return; + House.ImageUrl = e.Value?.ToString(); + await SaveHouseAsync(); + } + private void ShowAddModal() { _modalEntry = HouseHistoryForm.NewEntry(Id); diff --git a/src/BlazorApp/Components/Inventory/Pages/Houses.razor b/src/BlazorApp/Components/Inventory/Pages/Houses.razor index 4eb9a0ef..fcdb5fae 100644 --- a/src/BlazorApp/Components/Inventory/Pages/Houses.razor +++ b/src/BlazorApp/Components/Inventory/Pages/Houses.razor @@ -17,6 +17,8 @@ ShowForm="@_showForm" Form="@_form" ItemTitle="@(house => house.Name)" + ItemImageUrl="@(house => house.ImageUrl)" + ItemImageShape="wide" OnSearchKeyUp="@OnSearchKeyUp" OnClearSearch="@ClearSearch" OnGoToPage="@GoToPage" diff --git a/src/BlazorApp/Components/Inventory/Pages/MovieDetail.razor b/src/BlazorApp/Components/Inventory/Pages/MovieDetail.razor index f391c731..76c8a5a0 100644 --- a/src/BlazorApp/Components/Inventory/Pages/MovieDetail.razor +++ b/src/BlazorApp/Components/Inventory/Pages/MovieDetail.razor @@ -31,6 +31,17 @@ else title="@(string.IsNullOrEmpty(Movie.ReferenceId) ? "Check for reference match using the title and year below." : $"Linked to \"{Reference?.Title}\". Not right? Edit the title or year below, then check again.")"> + @if (!string.IsNullOrEmpty(Movie.ReferenceId)) + { + + + + + + } @if (_refreshMessage is not null) { @_refreshMessage @@ -51,6 +62,12 @@ else } + +
@(Movie.FirstSeenAt is not null ? "✓ Watched" : "Mark as watched") @@ -136,6 +153,7 @@ else private string? _refreshMessage; private string _refreshMessageStyle = "neutral"; private object? _refreshMessageToken; + private bool _pendingUnlink; protected override async Task OnParametersSetAsync() { @@ -207,7 +225,7 @@ else private async Task SetWatchedDateAsync(ChangeEventArgs e) { if (Movie is null) return; - Movie.FirstSeenAt = DateOnly.TryParse(e.Value?.ToString(), out var date) ? date : null; + Movie.FirstSeenAt = DateOnlyInput.Parse(e.Value); await MovieApi.UpdateAsync(Movie); } @@ -232,9 +250,7 @@ else var refreshed = await MovieApi.RefreshReferenceAsync(Movie.Id); await LoadAsync(); - (_refreshMessage, _refreshMessageStyle) = string.IsNullOrEmpty(refreshed.ReferenceId) - ? string.IsNullOrEmpty(previousReferenceId) ? ("No match found", "neutral") : ("Unlinked - no match", "danger") - : refreshed.ReferenceId == previousReferenceId ? ("Already linked", "neutral") : ("Linked!", "success"); + (_refreshMessage, _refreshMessageStyle) = ReferenceRefreshMessage.Compute(previousReferenceId, refreshed.ReferenceId); } catch (Exception ex) { @@ -249,6 +265,26 @@ else ScheduleRefreshMessageClear(); } + private async Task UnlinkReferenceAsync() + { + _pendingUnlink = false; + if (Movie?.Id is null) return; + + try + { + await MovieApi.UnlinkReferenceAsync(Movie.Id); + await LoadAsync(); + (_refreshMessage, _refreshMessageStyle) = ("Unlinked and reference removed.", "success"); + } + catch (Exception ex) + { + _refreshMessage = $"Error: {ex.Message}"; + _refreshMessageStyle = "danger"; + } + + ScheduleRefreshMessageClear(); + } + /// /// The toast is a small, auto-dismissing pill next to the refresh icon, not a permanent line of text - /// a token guards against an in-flight delay from a previous click clearing a newer message. diff --git a/src/BlazorApp/Components/Inventory/Pages/TvShowDetail.razor b/src/BlazorApp/Components/Inventory/Pages/TvShowDetail.razor index f350c04d..917d37db 100644 --- a/src/BlazorApp/Components/Inventory/Pages/TvShowDetail.razor +++ b/src/BlazorApp/Components/Inventory/Pages/TvShowDetail.razor @@ -13,7 +13,7 @@
} } -else if (Show is null) +else if (_show is null) {
@@ -22,86 +22,103 @@ else if (Show is null) } else { - +
- + + @if (!string.IsNullOrEmpty(_show.ReferenceId)) + { + + + + + + } @if (_refreshMessage is not null) { @_refreshMessage }
- - - + + +
- - - + + +
- @if (string.IsNullOrEmpty(Show.ReferenceId)) + @if (string.IsNullOrEmpty(_show.ReferenceId)) { - + } + +
- @if (!string.IsNullOrEmpty(Reference?.ImageUrl)) + @if (!string.IsNullOrEmpty(_reference?.ImageUrl)) {
- @Show.Title poster + @_show.Title poster
}
+ value="@_show.Year" @onchange="SaveYearAsync"/>
- +
- @if (Reference?.Genres.Count > 0) + @if (_reference?.Genres.Count > 0) { -

@string.Join(", ", Reference.Genres)

+

@string.Join(", ", _reference.Genres)

}
- +
- @if (!string.IsNullOrEmpty(Reference?.Synopsis)) + @if (!string.IsNullOrEmpty(_reference?.Synopsis)) {
-

@Reference.Synopsis

+

@_reference.Synopsis

}
- @if (Reference?.Cast.Count > 0) + @if (_reference?.Cast.Count > 0) {

Cast

- + } - + - @if (Reference is not null) + @if (_reference is not null) { @if (_seasons.Count == 0) { @@ -221,20 +238,9 @@ else // of a spinner flash on every navigation to this page. private bool _loading; private bool _loaded; - - // Public properties (a framework requirement for [PersistentState]): the data loaded during the - // prerender pass is carried over to the interactive circuit, so the first interactive render reuses - // it instead of resetting to the spinner and re-fetching - same pattern as MovieDetail. The derived - // season/watched lookups are rebuilt locally from these (a tuple-keyed dictionary can't round-trip - // through the JSON-serialized persisted state). - [PersistentState] - public TvShowDto? Show { get; set; } - - [PersistentState] - public TvShowReferenceDto? Reference { get; set; } - - [PersistentState] - public List? Episodes { get; set; } + private TvShowDto? _show; + private TvShowReferenceDto? _reference; + private List? _episodes; private Dictionary<(int Season, int Episode), EpisodeDto> _watchedByKey = new(); private Dictionary> _referenceEpisodesBySeason = new(); private List _seasons = []; @@ -247,24 +253,13 @@ else private string? _refreshMessage; private string _refreshMessageStyle = "neutral"; private object? _refreshMessageToken; + private bool _pendingUnlink; private int _newSeason = 1; private int _newEpisode = 1; private DateOnly? _newWatchedAt = DateOnly.FromDateTime(DateTime.Today); - protected override async Task OnParametersSetAsync() - { - // Show already holds this route's item when PersistentState restored the prerendered data - // the id check keeps an in-circuit navigation to a different show reloading as before - if (Show?.Id == Id && Episodes is not null) - { - BuildDerivedState(); - _loading = false; - _loaded = true; - return; - } - await LoadAsync(); - } + protected override async Task OnParametersSetAsync() => await LoadAsync(); private async Task LoadAsync() { @@ -275,28 +270,23 @@ else private async Task FetchAsync() { - Show = await TvShowApi.GetOneAsync(Id); + _show = await TvShowApi.GetOneAsync(Id); var episodes = await EpisodeApi.GetAsync(string.Empty, 1, 5000, new Dictionary { ["TvShowId"] = Id }); - Episodes = episodes.Items; + _episodes = episodes.Items; // reference data (synopsis, real episode titles, cast, poster) is a progressive enhancement: it // may not exist yet (background match still pending, or genuinely unresolved), so the page never // blocks on it - see the two very different rendering branches below. - Reference = string.IsNullOrEmpty(Show?.ReferenceId) ? null : await ReferenceDataApi.GetTvShowAsync(Show.ReferenceId); - BuildDerivedState(); - } + _reference = string.IsNullOrEmpty(_show?.ReferenceId) ? null : await ReferenceDataApi.GetTvShowAsync(_show.ReferenceId); + _watchedByKey = _episodes.ToDictionary(e => (e.SeasonNumber, e.EpisodeNumber)); - private void BuildDerivedState() - { - _watchedByKey = Episodes!.ToDictionary(e => (e.SeasonNumber, e.EpisodeNumber)); - - if (Reference is not null) + if (_reference is not null) { // full checklist, in natural watch-through order - an episode TMDB lists with a future air date // hasn't aired yet, so it isn't "unseen and ready to watch", it doesn't exist yet from the // viewer's perspective (same air-date filter WatchNextService applies for the "next episode" calc) var today = DateOnly.FromDateTime(DateTime.Today); - _referenceEpisodesBySeason = Reference.Episodes + _referenceEpisodesBySeason = _reference.Episodes .Where(e => e.AirDate is null || e.AirDate <= today) .GroupBy(e => e.SeasonNumber) .ToDictionary(g => g.Key, g => g.OrderBy(e => e.EpisodeNumber).ToList()); @@ -306,7 +296,7 @@ else else { // unresolved: only what's actually been recorded, most recently aired first - _unresolvedEpisodesBySeason = Episodes! + _unresolvedEpisodesBySeason = _episodes .GroupBy(e => e.SeasonNumber) .ToDictionary(g => g.Key, g => g.OrderByDescending(e => e.EpisodeNumber).ToList()); _unresolvedSeasons = _unresolvedEpisodesBySeason.Keys.OrderByDescending(s => s).ToList(); @@ -318,36 +308,36 @@ else private async Task ToggleFavoriteAsync() { - if (Show is null) return; - Show.IsFavorite = !Show.IsFavorite; - await TvShowApi.UpdateAsync(Show); + if (_show is null) return; + _show.IsFavorite = !_show.IsFavorite; + await TvShowApi.UpdateAsync(_show); } private async Task ToggleWantToWatchAsync() { - if (Show is null) return; - Show.WantToWatch = !Show.WantToWatch; - await TvShowApi.UpdateAsync(Show); + if (_show is null) return; + _show.WantToWatch = !_show.WantToWatch; + await TvShowApi.UpdateAsync(_show); } private async Task SaveShowAsync() { - if (Show is null) return; - await TvShowApi.UpdateAsync(Show); + if (_show is null) return; + await TvShowApi.UpdateAsync(_show); } private async Task ToggleWishlistedAsync() { - if (Show is null) return; - Show.IsWishlisted = !Show.IsWishlisted; - await TvShowApi.UpdateAsync(Show); + if (_show is null) return; + _show.IsWishlisted = !_show.IsWishlisted; + await TvShowApi.UpdateAsync(_show); } private async Task SetRatingAsync(float rating) { - if (Show is null) return; - Show.Rating = rating; - await TvShowApi.UpdateAsync(Show); + if (_show is null) return; + _show.Rating = rating; + await TvShowApi.UpdateAsync(_show); } /// @@ -357,35 +347,33 @@ else /// private async Task SetStateAsync(TvShowStatus state) { - if (Show is null) return; - Show.State = Show.State == state ? null : state; - await TvShowApi.UpdateAsync(Show); + if (_show is null) return; + _show.State = _show.State == state ? null : state; + await TvShowApi.UpdateAsync(_show); } private async Task SaveTitleAsync(ChangeEventArgs e) { - if (Show is null) return; + if (_show is null) return; var title = e.Value?.ToString(); if (string.IsNullOrWhiteSpace(title)) return; - Show.Title = title; - await TvShowApi.UpdateAsync(Show); + _show.Title = title; + await TvShowApi.UpdateAsync(_show); } private async Task RefreshReferenceAsync() { - if (Show?.Id is null) return; + if (_show?.Id is null) return; - var previousReferenceId = Show.ReferenceId; + var previousReferenceId = _show.ReferenceId; _refreshingReference = true; _refreshMessage = null; try { - var refreshed = await TvShowApi.RefreshReferenceAsync(Show.Id); + var refreshed = await TvShowApi.RefreshReferenceAsync(_show.Id); await LoadAsync(); - (_refreshMessage, _refreshMessageStyle) = string.IsNullOrEmpty(refreshed.ReferenceId) - ? string.IsNullOrEmpty(previousReferenceId) ? ("No match found", "neutral") : ("Unlinked - no match", "danger") - : refreshed.ReferenceId == previousReferenceId ? ("Already linked", "neutral") : ("Linked!", "success"); + (_refreshMessage, _refreshMessageStyle) = ReferenceRefreshMessage.Compute(previousReferenceId, refreshed.ReferenceId); } catch (Exception ex) { @@ -400,6 +388,26 @@ else ScheduleRefreshMessageClear(); } + private async Task UnlinkReferenceAsync() + { + _pendingUnlink = false; + if (_show?.Id is null) return; + + try + { + await TvShowApi.UnlinkReferenceAsync(_show.Id); + await LoadAsync(); + (_refreshMessage, _refreshMessageStyle) = ("Unlinked and reference removed.", "success"); + } + catch (Exception ex) + { + _refreshMessage = $"Error: {ex.Message}"; + _refreshMessageStyle = "danger"; + } + + ScheduleRefreshMessageClear(); + } + /// /// The toast is a small, auto-dismissing pill next to the refresh icon, not a permanent line of text - /// a token guards against an in-flight delay from a previous click clearing a newer message. @@ -421,16 +429,16 @@ else private async Task SaveYearAsync(ChangeEventArgs e) { - if (Show is null) return; - Show.Year = int.TryParse(e.Value?.ToString(), out var year) ? year : null; - await TvShowApi.UpdateAsync(Show); + if (_show is null) return; + _show.Year = int.TryParse(e.Value?.ToString(), out var year) ? year : null; + await TvShowApi.UpdateAsync(_show); } private async Task SaveNotesAsync(ChangeEventArgs e) { - if (Show is null) return; - Show.Notes = e.Value?.ToString(); - await TvShowApi.UpdateAsync(Show); + if (_show is null) return; + _show.Notes = e.Value?.ToString(); + await TvShowApi.UpdateAsync(_show); } private bool IsSelectedSeasonFullyWatched() => diff --git a/src/BlazorApp/Components/Inventory/Pages/VideoGameDetail.razor b/src/BlazorApp/Components/Inventory/Pages/VideoGameDetail.razor index 9623c5ec..4455998f 100644 --- a/src/BlazorApp/Components/Inventory/Pages/VideoGameDetail.razor +++ b/src/BlazorApp/Components/Inventory/Pages/VideoGameDetail.razor @@ -31,6 +31,17 @@ else title="@(string.IsNullOrEmpty(Game.ReferenceId) ? "Check for reference match using the title and year below." : $"Linked to \"{Reference?.Title}\". Not right? Edit the title or year below, then check again.")"> + @if (!string.IsNullOrEmpty(Game.ReferenceId)) + { + + + + + + } @if (_refreshMessage is not null) { @_refreshMessage @@ -50,10 +61,17 @@ else } - @if (!string.IsNullOrEmpty(Reference?.ImageUrl)) + + + var coverUrl = !string.IsNullOrEmpty(Game.CustomImageUrl) ? Game.CustomImageUrl : Reference?.ImageUrl; + @if (!string.IsNullOrEmpty(coverUrl)) {
- @Game.Title cover + @Game.Title cover
} @@ -77,6 +95,11 @@ else {

@string.Join(", ", Reference.Genres)

} +
+ + +
@@ -113,6 +136,13 @@ else + +
+ + +
+
@@ -213,6 +243,7 @@ else private object? _refreshMessageToken; private VideoGamePlatformDto? _draftPlatform; private VideoGamePlatformDto? _pendingRemovePlatform; + private bool _pendingUnlink; protected override async Task OnParametersSetAsync() { @@ -285,6 +316,13 @@ else await SaveGameAsync(); } + private async Task SaveCustomImageUrlAsync(ChangeEventArgs e) + { + if (Game is null) return; + Game.CustomImageUrl = e.Value?.ToString(); + await SaveGameAsync(); + } + private void StartDraftPlatform() => _draftPlatform = new VideoGamePlatformDto(); private void CancelDraftPlatform() => _draftPlatform = null; @@ -335,7 +373,8 @@ else && entry.Price is null && entry.AcquiredAt is null && string.IsNullOrWhiteSpace(entry.Vendor) - && string.IsNullOrWhiteSpace(entry.Reference); + && string.IsNullOrWhiteSpace(entry.Reference) + && string.IsNullOrWhiteSpace(entry.ProductName); /// /// Clicking the already-active state button clears it back to unset - same toggle-off-on-reclick @@ -350,7 +389,13 @@ else private async Task SetPlatformCompletedDateAsync(VideoGamePlatformDto entry, ChangeEventArgs e) { - entry.CompletedAt = DateOnly.TryParse(e.Value?.ToString(), out var date) ? date : null; + entry.CompletedAt = DateOnlyInput.Parse(e.Value); + await SaveGameAsync(); + } + + private async Task SetPlatformProductNameAsync(VideoGamePlatformDto entry, ChangeEventArgs e) + { + entry.ProductName = e.Value?.ToString(); await SaveGameAsync(); } @@ -363,7 +408,7 @@ else private async Task SetFullyCompletedDateAsync(VideoGamePlatformDto entry, ChangeEventArgs e) { - entry.FullyCompletedAt = DateOnly.TryParse(e.Value?.ToString(), out var date) ? date : null; + entry.FullyCompletedAt = DateOnlyInput.Parse(e.Value); await SaveGameAsync(); } @@ -387,7 +432,7 @@ else private async Task UpdatePlaythroughDateAsync(PlaythroughDto playthrough, ChangeEventArgs e) { - playthrough.CompletedAt = DateOnly.TryParse(e.Value?.ToString(), out var date) ? date : null; + playthrough.CompletedAt = DateOnlyInput.Parse(e.Value); await SaveGameAsync(); } @@ -403,9 +448,7 @@ else var refreshed = await VideoGameApi.RefreshReferenceAsync(Game.Id); await LoadAsync(); - (_refreshMessage, _refreshMessageStyle) = string.IsNullOrEmpty(refreshed.ReferenceId) - ? string.IsNullOrEmpty(previousReferenceId) ? ("No match found", "neutral") : ("Unlinked - no match", "danger") - : refreshed.ReferenceId == previousReferenceId ? ("Already linked", "neutral") : ("Linked!", "success"); + (_refreshMessage, _refreshMessageStyle) = ReferenceRefreshMessage.Compute(previousReferenceId, refreshed.ReferenceId); } catch (Exception ex) { @@ -420,6 +463,26 @@ else ScheduleRefreshMessageClear(); } + private async Task UnlinkReferenceAsync() + { + _pendingUnlink = false; + if (Game?.Id is null) return; + + try + { + await VideoGameApi.UnlinkReferenceAsync(Game.Id); + await LoadAsync(); + (_refreshMessage, _refreshMessageStyle) = ("Unlinked and reference removed.", "success"); + } + catch (Exception ex) + { + _refreshMessage = $"Error: {ex.Message}"; + _refreshMessageStyle = "danger"; + } + + ScheduleRefreshMessageClear(); + } + /// /// The toast is a small, auto-dismissing pill next to the refresh icon, not a permanent line of text - /// a token guards against an in-flight delay from a previous click clearing a newer message. diff --git a/src/BlazorApp/Components/Inventory/Shared/OwnedVersionFields.razor b/src/BlazorApp/Components/Inventory/Shared/OwnedVersionFields.razor index e97c602f..eb816096 100644 --- a/src/BlazorApp/Components/Inventory/Shared/OwnedVersionFields.razor +++ b/src/BlazorApp/Components/Inventory/Shared/OwnedVersionFields.razor @@ -1,11 +1,18 @@ @using System.Globalization +@inject UserPreferencesState PreferencesState @* Shared owned-copy field body (Physical/Digital toggle + price/acquired/vendor/reference) - used by OwnedVersionsEditor.razor (movie/TV show/book/album), VideoGameDetail.razor's per-platform cards, and Quick Add's own owned-copy toggle. Each caller keeps its own persistence timing (draft Save button vs. auto-save vs. nothing-persists-until-Save) via OnChanged, which defaults to a no-op. ButtonRowSuffix renders alongside the Physical/Digital buttons (e.g. a caller's own remove icon), so a - caller's header chrome doesn't need to be split awkwardly around this component. *@ + caller's header chrome doesn't need to be split awkwardly around this component. + ExtraFields renders as one more column in the same field row (e.g. VideoGamePlatformDto.ProductName, + which has no equivalent on the shared IOwnedCopyDto - not every caller has an extra field like this, so + this stays a slot rather than a member on the interface). The four columns below use unnumbered col-md + (equal-width auto layout) rather than a fixed col-md-3 for exactly this reason: with no ExtraFields they + still fill one row identically to a fixed 3/3/3/3 split, but a 5th ExtraFields column joins the same row + and everyone re-shares the width automatically, instead of wrapping to a second row. *@
@@ -16,26 +23,36 @@
@* the euro sign is a display choice, not stored data - a per-user currency setting may replace it later *@ -
+
-
+
-
+
-
+
- +
+ + @if (_showAmazonProductLink && AmazonReference.TryExtractAsin(Copy.Reference) is { } asin) + { + + + + } +
+ @ExtraFields
@code { @@ -45,6 +62,15 @@ [Parameter] public RenderFragment? ButtonRowSuffix { get; set; } + [Parameter] public RenderFragment? ExtraFields { get; set; } + + private bool _showAmazonProductLink; + + protected override async Task OnInitializedAsync() + { + _showAmazonProductLink = (await PreferencesState.GetAsync()).Features.ShowAmazonProductLink; + } + private Task SetCopyTypeAsync(CopyType copyType) { Copy.CopyType = copyType; @@ -66,7 +92,7 @@ private Task SetAcquiredAtAsync(ChangeEventArgs e) { - Copy.AcquiredAt = DateOnly.TryParse(e.Value?.ToString(), out var date) ? date : null; + Copy.AcquiredAt = DateOnlyInput.Parse(e.Value); return OnChanged.InvokeAsync(); } diff --git a/src/BlazorApp/Components/Inventory/Shared/OwnedVersionsEditor.razor b/src/BlazorApp/Components/Inventory/Shared/OwnedVersionsEditor.razor index 60df46ec..4be083ea 100644 --- a/src/BlazorApp/Components/Inventory/Shared/OwnedVersionsEditor.razor +++ b/src/BlazorApp/Components/Inventory/Shared/OwnedVersionsEditor.razor @@ -34,6 +34,13 @@ } + +
+ + +
+
@if (isDraft) { @@ -112,7 +119,14 @@ version.Price is null && version.AcquiredAt is null && string.IsNullOrWhiteSpace(version.Vendor) - && string.IsNullOrWhiteSpace(version.Reference); + && string.IsNullOrWhiteSpace(version.Reference) + && string.IsNullOrWhiteSpace(version.ProductName); + + private async Task SetProductNameAsync(OwnedVersionDto version, ChangeEventArgs e) + { + version.ProductName = e.Value?.ToString(); + await NotifyChangedAsync(version); + } /// Saves after a mutation on a saved copy; a draft's edits stay local until its Save button. private async Task NotifyChangedAsync(OwnedVersionDto version) diff --git a/src/BlazorApp/Components/Inventory/Shared/SuggestInput.razor b/src/BlazorApp/Components/Inventory/Shared/SuggestInput.razor new file mode 100644 index 00000000..b4f71884 --- /dev/null +++ b/src/BlazorApp/Components/Inventory/Shared/SuggestInput.razor @@ -0,0 +1,106 @@ +@* Free-text input with a filtered, click-to-fill suggestion dropdown - not a closed choice, the app's own + styled stand-in for an HTML (whose popup is unstyleable native chrome that clashes with the + dark theme and floats over content below it - see GearDetail.razor's Category field, the first caller). *@ +
+ + @if (_open && FilteredSuggestions.Any()) + { +
    + @foreach (var suggestion in FilteredSuggestions) + { +
  • @suggestion
  • + } +
+ } +
+ +@code { + private const int MaxSuggestions = 8; + + [Parameter] public string? Value { get; set; } + + [Parameter] public EventCallback ValueChanged { get; set; } + + [Parameter] public IReadOnlyList Suggestions { get; set; } = []; + + [Parameter] public string? Placeholder { get; set; } + + /// Applied to the wrapper div - lets a caller size this differently than the default + /// full-width detail-page field (e.g. Gear.razor's fixed-width list-filter usage). + [Parameter] public string? CssClass { get; set; } + + private string _query = ""; + private string? _lastValueParam; + private bool _open; + + // guards OnBlurAsync's delayed close against a suggestion click that's still in flight - see SelectAsync + private bool _suppressBlurClose; + + private IEnumerable FilteredSuggestions => + Suggestions.Where(s => string.IsNullOrEmpty(_query) || s.Contains(_query, StringComparison.OrdinalIgnoreCase)).Take(MaxSuggestions); + + protected override void OnParametersSet() + { + // only adopt an externally-changed Value - typing of our own must never be clobbered by the + // re-render our own CommitAsync triggers, same "sent vs received" shape as InventoryList's search box + if (Value == _lastValueParam) return; + _lastValueParam = Value; + _query = Value ?? ""; + } + + private void OnFocus() => _open = true; + + private void OnInput(ChangeEventArgs e) + { + _query = e.Value?.ToString() ?? ""; + _open = true; + } + + /// + /// A suggestion is a plain, non-focusable <li> rather than a <button>, so clicking it never + /// steals focus from the input in the first place - the input's blur handler still fires on a real + /// blur (tab away, click elsewhere), just not from this click. The delay/flag pair only has to cover + /// touch, where a tap can blur before the click event finishes. + /// + private async Task SelectAsync(string suggestion) + { + _suppressBlurClose = true; + _query = suggestion; + _open = false; + await CommitAsync(suggestion); + } + + private async Task OnBlurAsync() + { + await Task.Delay(150); + if (_suppressBlurClose) + { + _suppressBlurClose = false; + return; + } + _open = false; + await CommitAsync(_query); + } + + private async Task OnKeyDownAsync(KeyboardEventArgs e) + { + switch (e.Key) + { + case "Escape": + _open = false; + break; + case "Enter": + _open = false; + await CommitAsync(_query); + break; + } + } + + private async Task CommitAsync(string? value) + { + var normalized = string.IsNullOrWhiteSpace(value) ? null : value; + if (normalized == Value) return; + await ValueChanged.InvokeAsync(normalized); + } +} diff --git a/src/BlazorApp/Components/Layout/NavMenu.razor b/src/BlazorApp/Components/Layout/NavMenu.razor index 84706af8..0bfe4727 100644 --- a/src/BlazorApp/Components/Layout/NavMenu.razor +++ b/src/BlazorApp/Components/Layout/NavMenu.razor @@ -80,6 +80,16 @@ Health
+ + } else @@ -94,7 +96,7 @@ private long TotalItems => Stats is null ? 0 - : Stats.Movies + Stats.TvShows + Stats.Books + Stats.Albums + Stats.VideoGames + Stats.Playlists + Stats.Cars + Stats.Houses; + : Stats.Movies + Stats.TvShows + Stats.Books + Stats.Albums + Stats.VideoGames + Stats.Playlists + Stats.Cars + Stats.Houses + Stats.Collectibles + Stats.Gear; protected override async Task OnInitializedAsync() { diff --git a/src/BlazorApp/Components/ReferenceDataAdmin/InlineReferenceLinker.razor b/src/BlazorApp/Components/ReferenceDataAdmin/InlineReferenceLinker.razor index bf676dd1..dc073242 100644 --- a/src/BlazorApp/Components/ReferenceDataAdmin/InlineReferenceLinker.razor +++ b/src/BlazorApp/Components/ReferenceDataAdmin/InlineReferenceLinker.razor @@ -29,7 +29,7 @@ {
Searching…
} - else if (_searched && _results.Count == 0) + else if (_searched && _results.Count == 0 && _error is null) {
No @ProviderName results for this title/year.
} @@ -126,8 +126,11 @@ { // an uncaught exception here would crash the whole Blazor Server circuit (not just this // component), forcing a full page reload - same reasoning as LinkAsync's own try/catch below. + // The provider's own exchange (which HTTP client, which status code) is an implementation + // detail the admin shouldn't have to parse out of a raw HttpRequestException message - + // "Search failed" plus a retry hint says the one thing that's actually actionable. _results = []; - _error = ex.Message; + _error = $"{ProviderName} search failed ({ex.Message}). This is usually transient - wait a moment and try again."; } finally { diff --git a/src/BlazorApp/Components/Shared/DateOnlyInput.cs b/src/BlazorApp/Components/Shared/DateOnlyInput.cs new file mode 100644 index 00000000..4e6d766a --- /dev/null +++ b/src/BlazorApp/Components/Shared/DateOnlyInput.cs @@ -0,0 +1,15 @@ +using System.Globalization; + +namespace Keeptrack.BlazorApp.Components.Shared; + +/// +/// Shared parser for a plain HTML date input's onchange value - always "yyyy-MM-dd" per the HTML spec +/// regardless of the browser's locale, so this always parses with the invariant culture rather than the +/// current one. Used by every owned-copy/car-history/house-history/video-game-completion-style bound +/// ? field instead of duplicating the same TryParse ternary at each call site. +/// +public static class DateOnlyInput +{ + public static DateOnly? Parse(object? value) => + DateOnly.TryParse(value?.ToString(), CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? date : null; +} diff --git a/src/BlazorApp/Components/Shared/ExternalLinkIcon.razor b/src/BlazorApp/Components/Shared/ExternalLinkIcon.razor new file mode 100644 index 00000000..500a7def --- /dev/null +++ b/src/BlazorApp/Components/Shared/ExternalLinkIcon.razor @@ -0,0 +1,14 @@ +@* A monochrome inline SVG "open external link" glyph, same shape as TrashIcon - used for actions that + leave the app (e.g. the chasse-aux-livres.fr shortcut on BookDetail.razor). Wrap it in the action's own + button/anchor; this is just the glyph. *@ + + + +@code { + [Parameter] public int Size { get; set; } = 15; +} diff --git a/src/BlazorApp/Components/Shared/ReferenceRefreshMessage.cs b/src/BlazorApp/Components/Shared/ReferenceRefreshMessage.cs new file mode 100644 index 00000000..d4d5facf --- /dev/null +++ b/src/BlazorApp/Components/Shared/ReferenceRefreshMessage.cs @@ -0,0 +1,23 @@ +namespace Keeptrack.BlazorApp.Components.Shared; + +/// +/// Shared "check for reference match" result message for every reference-linked detail page's refresh-reference +/// button (Movie/TvShow/Book/Album/VideoGame) - keeps the message/style rule in one place instead of duplicating +/// the same four-way outcome across five detail pages. +/// +public static class ReferenceRefreshMessage +{ + public static (string Message, string Style) Compute(string? previousReferenceId, string? newReferenceId) + { + if (string.IsNullOrEmpty(newReferenceId)) + { + return string.IsNullOrEmpty(previousReferenceId) + ? ("No match found", "neutral") + : ("Unlinked - no match", "danger"); + } + + return newReferenceId == previousReferenceId + ? ("Already linked", "neutral") + : ("Linked!", "success"); + } +} diff --git a/src/BlazorApp/Components/Shared/SvgChartHelpers.cs b/src/BlazorApp/Components/Shared/SvgChartHelpers.cs index 27d2a5b2..663f11e3 100644 --- a/src/BlazorApp/Components/Shared/SvgChartHelpers.cs +++ b/src/BlazorApp/Components/Shared/SvgChartHelpers.cs @@ -29,6 +29,13 @@ public readonly record struct ChartGeometry( public static readonly ChartGeometry FullWidthGeometry = new(ViewWidth: 600, ViewHeight: 170, PlotLeft: 40, PlotRight: 588, PlotTop: 14, PlotBottom: 132); + private const string AttrStroke = "stroke"; + private const string AttrStrokeWidth = "stroke-width"; + private const string AttrVectorEffect = "vector-effect"; + private const string NonScalingStroke = "non-scaling-stroke"; + private const string AttrTextAnchor = "text-anchor"; + private const string AttrClass = "class"; + /// /// Draws a graduated X/Y axis pair (arrowhead, tick marks, tick labels, axis title). /// Ticks are computed by the caller, since what counts as an evenly-spaced value differs between a continuous line chart and a per-bar categorical one. @@ -65,9 +72,9 @@ public static void RenderAxes( builder.AddAttribute(seq++, "y1", plotBottom.ToString("F1")); builder.AddAttribute(seq++, "x2", plotLeft.ToString("F1")); builder.AddAttribute(seq++, "y2", plotTop.ToString("F1")); - builder.AddAttribute(seq++, "stroke", AxisColor); - builder.AddAttribute(seq++, "stroke-width", "1"); - builder.AddAttribute(seq++, "vector-effect", "non-scaling-stroke"); + builder.AddAttribute(seq++, AttrStroke, AxisColor); + builder.AddAttribute(seq++, AttrStrokeWidth, "1"); + builder.AddAttribute(seq++, AttrVectorEffect, NonScalingStroke); builder.AddAttribute(seq++, "marker-end", $"url(#{markerId})"); builder.CloseElement(); @@ -77,9 +84,9 @@ public static void RenderAxes( builder.AddAttribute(seq++, "y1", plotBottom.ToString("F1")); builder.AddAttribute(seq++, "x2", plotRight.ToString("F1")); builder.AddAttribute(seq++, "y2", plotBottom.ToString("F1")); - builder.AddAttribute(seq++, "stroke", AxisColor); - builder.AddAttribute(seq++, "stroke-width", "1"); - builder.AddAttribute(seq++, "vector-effect", "non-scaling-stroke"); + builder.AddAttribute(seq++, AttrStroke, AxisColor); + builder.AddAttribute(seq++, AttrStrokeWidth, "1"); + builder.AddAttribute(seq++, AttrVectorEffect, NonScalingStroke); builder.AddAttribute(seq++, "marker-end", $"url(#{markerId})"); builder.CloseElement(); @@ -90,16 +97,16 @@ public static void RenderAxes( builder.AddAttribute(seq++, "y1", y.ToString("F1")); builder.AddAttribute(seq++, "x2", plotLeft.ToString("F1")); builder.AddAttribute(seq++, "y2", y.ToString("F1")); - builder.AddAttribute(seq++, "stroke", AxisColor); - builder.AddAttribute(seq++, "stroke-width", "1"); - builder.AddAttribute(seq++, "vector-effect", "non-scaling-stroke"); + builder.AddAttribute(seq++, AttrStroke, AxisColor); + builder.AddAttribute(seq++, AttrStrokeWidth, "1"); + builder.AddAttribute(seq++, AttrVectorEffect, NonScalingStroke); builder.CloseElement(); builder.OpenElement(seq++, "text"); builder.AddAttribute(seq++, "x", (plotLeft - 5).ToString("F1")); builder.AddAttribute(seq++, "y", (y + 2.5).ToString("F1")); - builder.AddAttribute(seq++, "text-anchor", "end"); - builder.AddAttribute(seq++, "class", "kt-chart-axis-text"); + builder.AddAttribute(seq++, AttrTextAnchor, "end"); + builder.AddAttribute(seq++, AttrClass, "kt-chart-axis-text"); builder.AddContent(seq++, label); builder.CloseElement(); } @@ -111,16 +118,16 @@ public static void RenderAxes( builder.AddAttribute(seq++, "y1", plotBottom.ToString("F1")); builder.AddAttribute(seq++, "x2", x.ToString("F1")); builder.AddAttribute(seq++, "y2", (plotBottom + 3).ToString("F1")); - builder.AddAttribute(seq++, "stroke", AxisColor); - builder.AddAttribute(seq++, "stroke-width", "1"); - builder.AddAttribute(seq++, "vector-effect", "non-scaling-stroke"); + builder.AddAttribute(seq++, AttrStroke, AxisColor); + builder.AddAttribute(seq++, AttrStrokeWidth, "1"); + builder.AddAttribute(seq++, AttrVectorEffect, NonScalingStroke); builder.CloseElement(); builder.OpenElement(seq++, "text"); builder.AddAttribute(seq++, "x", x.ToString("F1")); builder.AddAttribute(seq++, "y", (plotBottom + 12).ToString("F1")); - builder.AddAttribute(seq++, "text-anchor", "middle"); - builder.AddAttribute(seq++, "class", "kt-chart-axis-text"); + builder.AddAttribute(seq++, AttrTextAnchor, "middle"); + builder.AddAttribute(seq++, AttrClass, "kt-chart-axis-text"); builder.AddContent(seq++, label); builder.CloseElement(); } @@ -129,8 +136,8 @@ public static void RenderAxes( builder.OpenElement(seq++, "text"); builder.AddAttribute(seq++, "x", "2"); builder.AddAttribute(seq++, "y", (plotTop - 4).ToString("F1")); - builder.AddAttribute(seq++, "text-anchor", "start"); - builder.AddAttribute(seq++, "class", "kt-chart-axis-title"); + builder.AddAttribute(seq++, AttrTextAnchor, "start"); + builder.AddAttribute(seq++, AttrClass, "kt-chart-axis-title"); builder.AddContent(seq++, yAxisLabel); builder.CloseElement(); @@ -138,8 +145,8 @@ public static void RenderAxes( builder.OpenElement(seq++, "text"); builder.AddAttribute(seq++, "x", xTitleCenter.ToString("F1")); builder.AddAttribute(seq++, "y", (viewHeight - 4).ToString("F1")); - builder.AddAttribute(seq++, "text-anchor", "middle"); - builder.AddAttribute(seq++, "class", "kt-chart-axis-title"); + builder.AddAttribute(seq++, AttrTextAnchor, "middle"); + builder.AddAttribute(seq++, AttrClass, "kt-chart-axis-title"); builder.AddContent(seq++, xAxisLabel); builder.CloseElement(); } diff --git a/src/BlazorApp/Components/WatchNext/WatchNextPage.razor b/src/BlazorApp/Components/WatchNext/WatchNextPage.razor index 78aad0d2..4b32c84e 100644 --- a/src/BlazorApp/Components/WatchNext/WatchNextPage.razor +++ b/src/BlazorApp/Components/WatchNext/WatchNextPage.razor @@ -99,6 +99,15 @@ else if (Data is not null) [Inject] private WatchNextApiClient WatchNextApi { get; set; } = null!; + [Inject] private NavigationManager Navigation { get; set; } = null!; + + // Persisted in the URL (?tab=) the same way list pages persist search/page/filters - see + // InventoryPageBase's ApplyQueryChanges/SetFilter for the pattern this mirrors. Unlike a list page's + // query params, changing this one never triggers a reload: Data isn't query-dependent here (WatchNextDto + // is one full, unpaged aggregate), so OnParametersSet only ever reparses the tab, it never calls LoadAsync. + [SupplyParameterFromQuery(Name = "tab")] + public string? TabQuery { get; set; } + // _loading is delay-gated (see LoadingIndicator) and only turns on for a load that's genuinely slow. // _loaded tracks whether a load attempt has finished at all - both default false so the forced // render Blazor triggers right after the synchronous prefix of OnInitializedAsync (before any @@ -117,6 +126,8 @@ else if (Data is not null) protected override Task OnInitializedAsync() => LoadAsync(); + protected override void OnParametersSet() => _tab = Enum.TryParse(TabQuery, out var tab) ? tab : Tab.TvShows; + private async Task LoadAsync() { await LoadingIndicator.RunAsync(FetchAsync(), v => _loading = v, StateHasChanged); @@ -129,5 +140,6 @@ else if (Data is not null) Data = await WatchNextApi.GetAsync(); } - private void SelectTab(Tab tab) => _tab = tab; + private void SelectTab(Tab tab) => + Navigation.NavigateTo(Navigation.GetUriWithQueryParameters(new Dictionary { ["tab"] = tab.ToString() })); } diff --git a/src/BlazorApp/Components/Wishlist/WishlistPage.razor b/src/BlazorApp/Components/Wishlist/WishlistPage.razor index fd125c29..a03d8f7b 100644 --- a/src/BlazorApp/Components/Wishlist/WishlistPage.razor +++ b/src/BlazorApp/Components/Wishlist/WishlistPage.razor @@ -110,6 +110,13 @@ else if (Data is not null) [Inject] private IJSRuntime JsRuntime { get; set; } = null!; + // Persisted in the URL (?tab=) the same way list pages persist search/page/filters - see + // InventoryPageBase's ApplyQueryChanges/SetFilter for the pattern this mirrors. Unlike a list page's + // query params, changing this one never triggers a reload: Data isn't query-dependent here (WishlistDto + // is one full, unpaged aggregate), so OnParametersSet only ever reparses the tab, it never calls LoadAsync. + [SupplyParameterFromQuery(Name = "tab")] + public string? TabQuery { get; set; } + // _loading is delay-gated (see LoadingIndicator) and only turns on for a load that's genuinely slow. // _loaded tracks whether a load attempt has finished at all - both default false so the forced // render Blazor triggers right after the synchronous prefix of OnInitializedAsync (before any @@ -136,6 +143,8 @@ else if (Data is not null) protected override Task OnInitializedAsync() => LoadAsync(); + protected override void OnParametersSet() => _tab = Enum.TryParse(TabQuery, out var tab) ? tab : Tab.Movies; + private async Task LoadAsync() { await LoadingIndicator.RunAsync(FetchAsync(), v => _loading = v, StateHasChanged); @@ -148,7 +157,8 @@ else if (Data is not null) Data = await WishlistApi.GetAsync(); } - private void SelectTab(Tab tab) => _tab = tab; + private void SelectTab(Tab tab) => + Navigation.NavigateTo(Navigation.GetUriWithQueryParameters(new Dictionary { ["tab"] = tab.ToString() })); private async Task ToggleSharePanelAsync() { diff --git a/src/BlazorApp/DependencyInjection/InfrastructureServiceCollectionExtensions.cs b/src/BlazorApp/DependencyInjection/InfrastructureServiceCollectionExtensions.cs index 53dff133..2ebff0f0 100644 --- a/src/BlazorApp/DependencyInjection/InfrastructureServiceCollectionExtensions.cs +++ b/src/BlazorApp/DependencyInjection/InfrastructureServiceCollectionExtensions.cs @@ -21,6 +21,10 @@ internal static void AddWebApiHttpClient(this IServiceCollection services, strin .AddHttpMessageHandler(); services.AddHttpClient(client => client.BaseAddress = webApiUri) .AddHttpMessageHandler(); + services.AddHttpClient(client => client.BaseAddress = webApiUri) + .AddHttpMessageHandler(); + services.AddHttpClient(client => client.BaseAddress = webApiUri) + .AddHttpMessageHandler(); services.AddHttpClient(client => client.BaseAddress = webApiUri) .AddHttpMessageHandler(); services.AddHttpClient(client => client.BaseAddress = webApiUri) @@ -43,12 +47,18 @@ internal static void AddWebApiHttpClient(this IServiceCollection services, strin .AddHttpMessageHandler(); services.AddHttpClient(client => client.BaseAddress = webApiUri) .AddHttpMessageHandler(); + services.AddHttpClient(client => client.BaseAddress = webApiUri) + .AddHttpMessageHandler(); + services.AddHttpClient(client => client.BaseAddress = webApiUri) + .AddHttpMessageHandler(); services.AddHttpClient(client => client.BaseAddress = webApiUri) .AddHttpMessageHandler(); services.AddHttpClient(client => client.BaseAddress = webApiUri) .AddHttpMessageHandler(); services.AddHttpClient(client => client.BaseAddress = webApiUri) .AddHttpMessageHandler(); + services.AddHttpClient(client => client.BaseAddress = webApiUri) + .AddHttpMessageHandler(); // deliberately NO AuthenticationTokenHandler: the shared-wishlist view is anonymous by design services.AddHttpClient(client => client.BaseAddress = webApiUri); } diff --git a/src/BlazorApp/Program.cs b/src/BlazorApp/Program.cs index f9dd2da6..19247232 100644 --- a/src/BlazorApp/Program.cs +++ b/src/BlazorApp/Program.cs @@ -43,6 +43,7 @@ } builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddWebApiHttpClient(builder.Configuration.TryGetSection("WebApi:BaseUrl")); builder.Services.AddHealthChecks(); diff --git a/src/BlazorApp/wwwroot/app.css b/src/BlazorApp/wwwroot/app.css index ba38720b..399b7aea 100644 --- a/src/BlazorApp/wwwroot/app.css +++ b/src/BlazorApp/wwwroot/app.css @@ -601,6 +601,28 @@ a.kt-item-row { color: inherit; text-decoration: none; } .kt-album-cover { width: 200px; height: 200px; } } +/* Gear/Collectible detail cover: full-width, on top of the fields (unlike .kt-album-hero's side-by-side + layout) - sized and fitted for e-shop-style product photography (e.g. an Amazon listing photo), which is + usually a product shot on a plain background rather than a poster/album-art image with a fixed aspect + ratio. "contain" (not "cover") so the product is never cropped. No background/border - just the image, + centered and capped to a sensible height. */ +.kt-product-cover-box { + width: 100%; + height: 480px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} +.kt-product-cover-box img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} +@media (max-width: 767px) { + .kt-product-cover-box { height: 320px; } +} + /* compact aligned row for a list of tracks/songs inside a .kt-form-card (e.g. a playlist's song list) - a CSS grid so title/artist/album line up in columns across rows without resorting to a full .kt-table-wrap/, which reads as a top-level list page rather than a detail-page sub-section. */ @@ -976,6 +998,60 @@ a.kt-item-row { color: inherit; text-decoration: none; } flex: 0 0 auto; } +/* Gear's category filter, sizing SuggestInput.razor's wrapper to a fixed compact width instead of the + .form-control default of 100% - a percentage width inside a flex row (like .kt-search-filters) resolves + against the row's own size, which is what pushed the whole filters row onto a wrapped second line for + no reason. Same fixed-width-flex-item shape as .kt-sort-select above, just wide enough for typed text. */ +.kt-category-select { + width: 150px; + flex: 0 0 auto; +} + +@media (max-width: 576px) { + .kt-category-select { width: 45vw; } +} + +/* ── Suggest input (SuggestInput.razor) ─────────────────────────────── + A plain text input with a filtered, click-to-fill suggestion menu - the app's own dark-themed + replacement for an HTML popup, which is unstyleable browser chrome that ignores + data-bs-theme and floats over whatever markup sits below it. */ +.kt-autocomplete { + position: relative; +} +.kt-autocomplete-menu { + position: absolute; + z-index: 20; + top: calc(100% + 4px); + left: 0; + right: 0; + margin: 0; + padding: 0.25rem; + list-style: none; + max-height: 220px; + overflow-y: auto; + background: var(--kt-surface-2); + border: 1px solid var(--kt-border); + border-radius: var(--kt-radius); + box-shadow: var(--kt-shadow); +} +.kt-autocomplete-item { + padding: 0.4rem 0.65rem; + border-radius: calc(var(--kt-radius) - 4px); + font-size: 0.85rem; + color: var(--kt-text); + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.kt-autocomplete-item:hover { + background: var(--kt-surface-3); +} +.kt-autocomplete-item.active { + background: var(--kt-accent-glow); + color: var(--kt-accent); +} + .kt-search-filters { display: flex; align-items: center; diff --git a/src/Common.System/AmazonReference.cs b/src/Common.System/AmazonReference.cs new file mode 100644 index 00000000..f9c57d50 --- /dev/null +++ b/src/Common.System/AmazonReference.cs @@ -0,0 +1,56 @@ +using System; +using System.Text.RegularExpressions; + +namespace Keeptrack.Common.System; + +/// +/// The read side of an Amazon-imported owned copy's Reference text (written by +/// AmazonImportMergeService.FormatOrderReference in the Domain project as +/// "Amazon order {orderId} (ASIN {asin})"). Lives here, not next to the writer, because it also +/// needs to be callable from BlazorApp (which can't reference Domain) to build an "open on Amazon" link - +/// keeping both halves of the same string shape in one place avoids the two drifting apart independently. +/// +public static partial class AmazonReference +{ + [GeneratedRegex(@"\(ASIN\s+([A-Za-z0-9]{10})\)")] + private static partial Regex AsinPattern(); + + /// + /// The ASIN embedded in an owned copy's Reference text, or null when it isn't there (most + /// copies weren't imported from Amazon at all). + /// + public static string? TryExtractAsin(string? reference) + { + if (string.IsNullOrEmpty(reference)) + { + return null; + } + + var match = AsinPattern().Match(reference); + return match.Success ? match.Groups[1].Value : null; + } + + /// + /// Amazon's own permalink shape (works on any of its marketplaces). is the + /// owned copy's own Vendor field - for an Amazon-imported copy this is the CSV export's "Website" + /// column (e.g. "Amazon.fr"), so a mixed-marketplace order history still opens on the right site + /// instead of always guessing one locale. Falls back to amazon.fr when the vendor isn't recognizably + /// an Amazon domain (a manually-entered copy, or an older import that didn't carry one). + /// + public static string BuildProductUrl(string asin, string? vendor) => $"https://{TryGetAmazonDomain(vendor) ?? "www.amazon.fr"}/dp/{asin}"; + + private static string? TryGetAmazonDomain(string? vendor) + { + var trimmed = vendor?.Trim(); + if (string.IsNullOrEmpty(trimmed) || !trimmed.Contains("amazon", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + var withoutScheme = SchemePrefix().Replace(trimmed, ""); + return withoutScheme.Split('/')[0].ToLowerInvariant(); + } + + [GeneratedRegex(@"^https?://", RegexOptions.IgnoreCase)] + private static partial Regex SchemePrefix(); +} diff --git a/src/Common.System/ListSort.cs b/src/Common.System/ListSort.cs index ea4fd8ea..bf83392f 100644 --- a/src/Common.System/ListSort.cs +++ b/src/Common.System/ListSort.cs @@ -22,4 +22,7 @@ public static class ListSort /// Most recently completed video game first (max CompletedAt across a game's platform entries); items with none last. public const string LastCompleted = "completed"; + + /// Most recently bought gear first (max AcquiredAt across a gear item's owned versions); items with none last. + public const string Bought = "bought"; } diff --git a/src/Domain/Domain.csproj b/src/Domain/Domain.csproj index ff76858c..9505dfaf 100644 --- a/src/Domain/Domain.csproj +++ b/src/Domain/Domain.csproj @@ -11,4 +11,10 @@ + + + + + diff --git a/src/Domain/Models/AlbumModel.cs b/src/Domain/Models/AlbumModel.cs index c4a57070..f93ebf88 100644 --- a/src/Domain/Models/AlbumModel.cs +++ b/src/Domain/Models/AlbumModel.cs @@ -21,6 +21,13 @@ public class AlbumModel : IHasIdAndOwnerId public string? ReferenceId { get; set; } + /// + /// Tenant-owned cover image override - takes priority over the linked reference's own cover wherever + /// a cover is shown (list thumbnail, detail page). Null means "use the reference's cover, if any" - + /// the previous, only behavior. + /// + public string? CustomImageUrl { get; set; } + public bool IsFavorite { get; set; } public List OwnedVersions { get; set; } = []; diff --git a/src/Domain/Models/AmazonOrderPreviewRow.cs b/src/Domain/Models/AmazonOrderPreviewRow.cs new file mode 100644 index 00000000..e2ebaa2d --- /dev/null +++ b/src/Domain/Models/AmazonOrderPreviewRow.cs @@ -0,0 +1,46 @@ +using System; + +namespace Keeptrack.Domain.Models; + +/// +/// One line item parsed from an Amazon order-history export, before the user has reviewed/selected it. +/// Transient - never persisted. Produced by and mapped +/// to AmazonOrderPreviewRowDto by AmazonOrderPreviewRowDtoMapper for the review UI. +/// +public class AmazonOrderPreviewRow +{ + /// + /// "{OrderId}:{Asin}" - stable within one export, used only to correlate a selected/edited row + /// back to this one at commit time. Never stored. + /// + public required string RowId { get; set; } + + /// Amazon's product name, with the export's mojibake repaired. + public required string Title { get; set; } + + public required string Asin { get; set; } + + public required string OrderId { get; set; } + + public DateOnly? OrderDate { get; set; } + + /// What was actually paid for this item - tax and any per-item discount already netted in. + public decimal? Price { get; set; } + + public required string Vendor { get; set; } + + /// Display only ("New", "Used"...) - not carried onto the created book. + public string? Condition { get; set; } + + /// True when passes an ISBN-10 checksum - the review table's default filter. + public bool LooksLikeBook { get; set; } + + /// again, only when is true. + public string? SuggestedIsbn { get; set; } + + /// + /// True when an existing book already has an owned version referencing this order - see + /// . + /// + public bool AlreadyImported { get; set; } +} diff --git a/src/Domain/Models/AmazonOwnedItemImportRequestItem.cs b/src/Domain/Models/AmazonOwnedItemImportRequestItem.cs new file mode 100644 index 00000000..d170795b --- /dev/null +++ b/src/Domain/Models/AmazonOwnedItemImportRequestItem.cs @@ -0,0 +1,26 @@ +namespace Keeptrack.Domain.Models; + +/// +/// One user-selected/edited row from the review UI, already translated from the web contract - the input +/// to for the domains +/// that use (Book, Movie, TvShow, Gear, Collectible). See +/// for VideoGame's own shape. +/// +public class AmazonOwnedItemImportRequestItem +{ + public required string Title { get; set; } + + /// + /// The title exactly as Amazon listed it (before any edit the user made in the review UI) - preserved + /// in the created item's notes, since reference-data linking is expected to overwrite + /// later. + /// + public required string AmazonTitle { get; set; } + + public int? Year { get; set; } + + /// Book-only (null for Movie/TvShow/Gear/Collectible) - see . + public string? Isbn { get; set; } + + public required OwnedVersionModel OwnedVersion { get; set; } +} diff --git a/src/Domain/Models/AmazonVideoGameImportRequestItem.cs b/src/Domain/Models/AmazonVideoGameImportRequestItem.cs new file mode 100644 index 00000000..1a53c3eb --- /dev/null +++ b/src/Domain/Models/AmazonVideoGameImportRequestItem.cs @@ -0,0 +1,18 @@ +namespace Keeptrack.Domain.Models; + +/// +/// VideoGame's own shape for - +/// has no concept, using +/// (with a required platform name Amazon's export can never supply) +/// instead. See for Book/Movie/TvShow's shared shape. +/// +public class AmazonVideoGameImportRequestItem +{ + public required string Title { get; set; } + + public required string AmazonTitle { get; set; } + + public int? Year { get; set; } + + public required VideoGamePlatformModel Platform { get; set; } +} diff --git a/src/Domain/Models/CarModel.cs b/src/Domain/Models/CarModel.cs index c4451401..014075a5 100644 --- a/src/Domain/Models/CarModel.cs +++ b/src/Domain/Models/CarModel.cs @@ -19,4 +19,9 @@ public class CarModel : IHasIdAndOwnerId public string? LicensePlate { get; set; } public required CarEnergyType EnergyType { get; set; } + + /// + /// Tenant-owned cover image URL, shown on the list thumbnail and detail page header. Optional. + /// + public string? ImageUrl { get; set; } } diff --git a/src/Domain/Models/CollectibleModel.cs b/src/Domain/Models/CollectibleModel.cs new file mode 100644 index 00000000..0cbd1ba0 --- /dev/null +++ b/src/Domain/Models/CollectibleModel.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Keeptrack.Common.System; + +namespace Keeptrack.Domain.Models; + +public class CollectibleModel : IHasIdAndOwnerId +{ + public string? Id { get; set; } + + public required string OwnerId { get; set; } + + public required string Title { get; set; } + + public string? Brand { get; set; } + + public int? Year { get; set; } + + public string? Notes { get; set; } + + /// + /// Tenant-owned cover image URL, shown on the list thumbnail and detail page header. Optional. + /// + public string? ImageUrl { get; set; } + + public bool IsFavorite { get; set; } + + public List OwnedVersions { get; set; } = []; + + /// + /// Filter-only: matches if is non-empty. Never persisted - see + /// . + /// + public bool IsOwned { get; set; } +} diff --git a/src/Domain/Models/GearModel.cs b/src/Domain/Models/GearModel.cs new file mode 100644 index 00000000..6838a53b --- /dev/null +++ b/src/Domain/Models/GearModel.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using Keeptrack.Common.System; + +namespace Keeptrack.Domain.Models; + +public class GearModel : IHasIdAndOwnerId +{ + public string? Id { get; set; } + + public required string OwnerId { get; set; } + + public required string Title { get; set; } + + public string? Brand { get; set; } + + /// + /// Free-text category (e.g. "Electronics", "Camping") - suggested from the tenant's other gear but + /// not a closed list. Unlike a blog's tags, one gear item has exactly one category. + /// + public string? Category { get; set; } + + public int? Year { get; set; } + + public string? Notes { get; set; } + + /// + /// Tenant-owned cover image URL, shown on the list thumbnail and detail page header. Optional. + /// + public string? ImageUrl { get; set; } + + public bool IsFavorite { get; set; } + + public List OwnedVersions { get; set; } = []; + + /// + /// Filter-only: matches if is non-empty. Never persisted - see + /// . + /// + public bool IsOwned { get; set; } +} diff --git a/src/Domain/Models/GenericVideoGameImportPreviewRow.cs b/src/Domain/Models/GenericVideoGameImportPreviewRow.cs new file mode 100644 index 00000000..47f7638a --- /dev/null +++ b/src/Domain/Models/GenericVideoGameImportPreviewRow.cs @@ -0,0 +1,34 @@ +using System; + +namespace Keeptrack.Domain.Models; + +/// +/// One line item parsed from a generic video game transaction-history CSV (store purchase history - PSN +/// today, any store exporting the same shape later), for the user to review before commit. See +/// . +/// +public class GenericVideoGameImportPreviewRow +{ + /// The transaction id - unique per line item in every export seen so far. + public required string RowId { get; set; } + + /// The game title, cleaned of a trailing platform suffix (e.g. "(PS4)") when present. + public required string Title { get; set; } + + public required string Platform { get; set; } + + /// The store's own specific product/edition text (e.g. "Grand Theft Auto V : Édition Premium"). + public string? ProductName { get; set; } + + public required string Vendor { get; set; } + + public required string TransactionId { get; set; } + + public required string OrderId { get; set; } + + public DateOnly? TransactionDate { get; set; } + + public decimal? Price { get; set; } + + public bool AlreadyImported { get; set; } +} diff --git a/src/Domain/Models/GenericVideoGameImportRequestItem.cs b/src/Domain/Models/GenericVideoGameImportRequestItem.cs new file mode 100644 index 00000000..9ec655c2 --- /dev/null +++ b/src/Domain/Models/GenericVideoGameImportRequestItem.cs @@ -0,0 +1,21 @@ +namespace Keeptrack.Domain.Models; + +/// +/// One user-selected/edited row from the generic video game import review UI, already translated from the +/// web contract - the input to . +/// +public class GenericVideoGameImportRequestItem +{ + public required string Title { get; set; } + + /// + /// The title exactly as the source export listed it (before any edit the user made in the review UI) - + /// preserved in the created item's notes, since reference-data linking is expected to overwrite + /// later. + /// + public required string SourceTitle { get; set; } + + public int? Year { get; set; } + + public required VideoGamePlatformModel Platform { get; set; } +} diff --git a/src/Domain/Models/HealthProfileModel.cs b/src/Domain/Models/HealthProfileModel.cs index 4299689d..ba0fdda1 100644 --- a/src/Domain/Models/HealthProfileModel.cs +++ b/src/Domain/Models/HealthProfileModel.cs @@ -16,4 +16,9 @@ public class HealthProfileModel : IHasIdAndOwnerId public required string Name { get; set; } public string? Notes { get; set; } + + /// + /// Tenant-owned cover image URL, shown on the list thumbnail and detail page header. Optional. + /// + public string? ImageUrl { get; set; } } diff --git a/src/Domain/Models/HouseModel.cs b/src/Domain/Models/HouseModel.cs index a35ab155..d322272b 100644 --- a/src/Domain/Models/HouseModel.cs +++ b/src/Domain/Models/HouseModel.cs @@ -22,4 +22,9 @@ public class HouseModel : IHasIdAndOwnerId public DateOnly? MovedOutAt { get; set; } public string? Notes { get; set; } + + /// + /// Tenant-owned cover image URL, shown on the list thumbnail and detail page header. Optional. + /// + public string? ImageUrl { get; set; } } diff --git a/src/Domain/Models/ImportCommitPlan.cs b/src/Domain/Models/ImportCommitPlan.cs new file mode 100644 index 00000000..52906464 --- /dev/null +++ b/src/Domain/Models/ImportCommitPlan.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace Keeptrack.Domain.Models; + +/// +/// What decided: +/// brand new items to create, and existing (or already-created-this-batch) items to update - each already +/// carrying the extra owned copy appended (an or a +/// , depending on ). +/// +public class ImportCommitPlan +{ + public List ItemsToCreate { get; set; } = []; + + public List ItemsToUpdate { get; set; } = []; + + /// + /// Every row that successfully got an owned copy added - the true per-row count, unlike + /// /'s counts, which are per distinct item. + /// Several selected rows sharing a title can consolidate into a single newly-created item within one + /// commit batch, which is not a data loss (every one of those rows still got its owned copy), but does + /// make ItemsToCreate.Count + ItemsToUpdate.Count come out lower than the number of selected rows. + /// OwnedCopiesAdded + OwnedCopiesSkipped always equals the number of rows submitted - the + /// reconciling total a caller can show the user to prove nothing was silently dropped. + /// + public int OwnedCopiesAdded { get; set; } + + /// A row whose reference already matched an existing owned copy - not duplicated. + public int OwnedCopiesSkipped { get; set; } + + /// The title of each row counted in , in submission order - lets a + /// caller show the user exactly which selected rows were treated as already-imported duplicates. + public List SkippedTitles { get; set; } = []; +} diff --git a/src/Domain/Models/OwnedVersionModel.cs b/src/Domain/Models/OwnedVersionModel.cs index e2bdf3ef..a98bc604 100644 --- a/src/Domain/Models/OwnedVersionModel.cs +++ b/src/Domain/Models/OwnedVersionModel.cs @@ -31,4 +31,10 @@ public class OwnedVersionModel /// Unrelated to the reference-data ReferenceId concept. /// public string? Reference { get; set; } + + /// + /// The store's own specific product/edition text for this copy (e.g. a retailer's listing title), + /// distinct from the item's own Title. Optional free text. + /// + public string? ProductName { get; set; } } diff --git a/src/Domain/Models/UserPreferencesFeaturesModel.cs b/src/Domain/Models/UserPreferencesFeaturesModel.cs new file mode 100644 index 00000000..efd6c0c3 --- /dev/null +++ b/src/Domain/Models/UserPreferencesFeaturesModel.cs @@ -0,0 +1,23 @@ +namespace Keeptrack.Domain.Models; + +/// +/// The actual opt-in/opt-out toggles held by . A new feature of +/// this kind adds its own named boolean property here, the same convention every other flag in this +/// codebase already follows (e.g. ) - not a generic settings dictionary. +/// +public class UserPreferencesFeaturesModel +{ + /// + /// Shows an "open in a new tab" link to https://www.chasse-aux-livres.fr/{isbn} next to a book's ISBN + /// field on BookDetail.razor, for users who use that site to price-check/verify French books. + /// + public bool ShowChasseAuxLivresLink { get; set; } + + /// + /// Shows an "open on Amazon" link next to an owned copy's Reference field + /// (OwnedVersionFields.razor, shared by every owned-copy type) whenever + /// finds an ASIN in it - i.e. the copy was + /// created by the Amazon order-history import. + /// + public bool ShowAmazonProductLink { get; set; } +} diff --git a/src/Domain/Models/UserPreferencesModel.cs b/src/Domain/Models/UserPreferencesModel.cs new file mode 100644 index 00000000..355023b0 --- /dev/null +++ b/src/Domain/Models/UserPreferencesModel.cs @@ -0,0 +1,17 @@ +using Keeptrack.Common.System; + +namespace Keeptrack.Domain.Models; + +/// +/// One document per user (see ). The opt-in/opt-out +/// toggles themselves live under , not as flat properties here, so a new one only +/// ever means adding a property to . +/// +public class UserPreferencesModel : IHasIdAndOwnerId +{ + public string? Id { get; set; } + + public required string OwnerId { get; set; } + + public UserPreferencesFeaturesModel Features { get; set; } = new(); +} diff --git a/src/Domain/Models/VideoGameModel.cs b/src/Domain/Models/VideoGameModel.cs index cdb1a999..717612b7 100644 --- a/src/Domain/Models/VideoGameModel.cs +++ b/src/Domain/Models/VideoGameModel.cs @@ -21,6 +21,13 @@ public class VideoGameModel : IHasIdAndOwnerId public string? ReferenceId { get; set; } + /// + /// Tenant-owned cover image override - takes priority over the linked reference's own cover wherever + /// a cover is shown (list thumbnail, detail page). Null means "use the reference's cover, if any" - + /// the previous, only behavior. + /// + public string? CustomImageUrl { get; set; } + /// /// Filter-only: matches if is non-empty. Never persisted - a platform entry /// (with its own ) is this type's owned copy, so ownership derives from having diff --git a/src/Domain/Models/VideoGamePlatformModel.cs b/src/Domain/Models/VideoGamePlatformModel.cs index d517d9ce..35436678 100644 --- a/src/Domain/Models/VideoGamePlatformModel.cs +++ b/src/Domain/Models/VideoGamePlatformModel.cs @@ -47,4 +47,11 @@ public class VideoGamePlatformModel /// Unrelated to the reference-data ReferenceId concept. /// public string? Reference { get; set; } + + /// + /// The store's own specific product/edition text for this copy (e.g. "Grand Theft Auto V : Édition + /// Premium"), as opposed to the game's own . Populated by the generic + /// video game transaction import from its "Product Name" column. + /// + public string? ProductName { get; set; } } diff --git a/src/Domain/Repositories/IAlbumReferenceRepository.cs b/src/Domain/Repositories/IAlbumReferenceRepository.cs index 449480a0..d2f81a3a 100644 --- a/src/Domain/Repositories/IAlbumReferenceRepository.cs +++ b/src/Domain/Repositories/IAlbumReferenceRepository.cs @@ -45,4 +45,10 @@ public interface IAlbumReferenceRepository /// full unpaged read is fine. /// Task> FindAllAsync(); + + /// + /// Permanently removes a reference document - backs the admin "unlink" action, which deletes the + /// shared document outright rather than merely detaching one tenant's link. + /// + Task DeleteAsync(string id); } diff --git a/src/Domain/Repositories/IBookReferenceRepository.cs b/src/Domain/Repositories/IBookReferenceRepository.cs index d19aea9d..919af338 100644 --- a/src/Domain/Repositories/IBookReferenceRepository.cs +++ b/src/Domain/Repositories/IBookReferenceRepository.cs @@ -45,4 +45,10 @@ public interface IBookReferenceRepository /// full unpaged read is fine. /// Task> FindAllAsync(); + + /// + /// Permanently removes a reference document - backs the admin "unlink" action, which deletes the + /// shared document outright rather than merely detaching one tenant's link. + /// + Task DeleteAsync(string id); } diff --git a/src/Domain/Repositories/ICollectibleRepository.cs b/src/Domain/Repositories/ICollectibleRepository.cs new file mode 100644 index 00000000..a9e243eb --- /dev/null +++ b/src/Domain/Repositories/ICollectibleRepository.cs @@ -0,0 +1,7 @@ +using Keeptrack.Domain.Models; + +namespace Keeptrack.Domain.Repositories; + +public interface ICollectibleRepository : IDataRepository +{ +} diff --git a/src/Domain/Repositories/IGearRepository.cs b/src/Domain/Repositories/IGearRepository.cs new file mode 100644 index 00000000..7c3bcfbd --- /dev/null +++ b/src/Domain/Repositories/IGearRepository.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Keeptrack.Domain.Models; + +namespace Keeptrack.Domain.Repositories; + +public interface IGearRepository : IDataRepository +{ + /// + /// Distinct, non-empty values across this tenant's gear, sorted + /// alphabetically - feeds the list page's category filter buttons and the detail page's suggested + /// values. One gear item has exactly one category (unlike a blog's tags), so this is a plain + /// distinct scan, not a tag-cloud aggregation. + /// + Task> FindDistinctCategoriesAsync(string ownerId); +} diff --git a/src/Domain/Repositories/IMovieReferenceRepository.cs b/src/Domain/Repositories/IMovieReferenceRepository.cs index 56c30465..1f6d41ed 100644 --- a/src/Domain/Repositories/IMovieReferenceRepository.cs +++ b/src/Domain/Repositories/IMovieReferenceRepository.cs @@ -39,4 +39,10 @@ public interface IMovieReferenceRepository /// full unpaged read is fine. /// Task> FindAllAsync(); + + /// + /// Permanently removes a reference document - backs the admin "unlink" action, which deletes the + /// shared document outright rather than merely detaching one tenant's link. + /// + Task DeleteAsync(string id); } diff --git a/src/Domain/Repositories/ITvShowReferenceRepository.cs b/src/Domain/Repositories/ITvShowReferenceRepository.cs index 48d1cf30..2b1cd6e8 100644 --- a/src/Domain/Repositories/ITvShowReferenceRepository.cs +++ b/src/Domain/Repositories/ITvShowReferenceRepository.cs @@ -40,4 +40,10 @@ public interface ITvShowReferenceRepository /// full unpaged read is fine. /// Task> FindAllAsync(); + + /// + /// Permanently removes a reference document - backs the admin "unlink" action, which deletes the + /// shared document outright rather than merely detaching one tenant's link. + /// + Task DeleteAsync(string id); } diff --git a/src/Domain/Repositories/IUserPreferencesRepository.cs b/src/Domain/Repositories/IUserPreferencesRepository.cs new file mode 100644 index 00000000..adc73eea --- /dev/null +++ b/src/Domain/Repositories/IUserPreferencesRepository.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using Keeptrack.Domain.Models; + +namespace Keeptrack.Domain.Repositories; + +/// +/// A single per-user preferences document, not a full owner-scoped CRUD collection (there's exactly one +/// document per owner, never listed/paged) - a small purpose-built repository like +/// , rather than forced through . +/// +public interface IUserPreferencesRepository +{ + /// + /// The owner's preferences, or null if they've never saved any - callers should fall back to an + /// all-default instance rather than writing one on read. + /// + Task FindByOwnerIdAsync(string ownerId); + + /// + /// Creates or fully replaces the owner's preferences document. + /// + Task UpsertAsync(UserPreferencesModel model); +} diff --git a/src/Domain/Repositories/IVideoGameReferenceRepository.cs b/src/Domain/Repositories/IVideoGameReferenceRepository.cs index 82d7479f..a772cd9a 100644 --- a/src/Domain/Repositories/IVideoGameReferenceRepository.cs +++ b/src/Domain/Repositories/IVideoGameReferenceRepository.cs @@ -38,4 +38,10 @@ public interface IVideoGameReferenceRepository /// full unpaged read is fine. /// Task> FindAllAsync(); + + /// + /// Permanently removes a reference document - backs the admin "unlink" action, which deletes the + /// shared document outright rather than merely detaching one tenant's link. + /// + Task DeleteAsync(string id); } diff --git a/src/Domain/Services/AmazonImportMergeService.cs b/src/Domain/Services/AmazonImportMergeService.cs new file mode 100644 index 00000000..27978c9f --- /dev/null +++ b/src/Domain/Services/AmazonImportMergeService.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace Keeptrack.Domain.Services; + +/// +/// Amazon-specific formatting used when committing a set of user-reviewed Amazon order rows: the ASIN-based +/// reference text and the provenance-notes text. The actual create/merge/dedup engine used to live here too, +/// but it was already fully generic - it moved to once the generic +/// video game transaction importer needed the exact same engine, leaving this class with only the two +/// members that are genuinely Amazon-specific. +/// +public static class AmazonImportMergeService +{ + /// + /// The one place that formats an owned copy's Reference for an imported order line - human-readable, + /// and also the exact-match dedup key + /// looks for on a later re-import. Includes the ASIN, not just the order id: a single Amazon order commonly + /// contains several different line items, and order-id-only matching (a real bug, found before this shipped) + /// meant a second, genuinely different item from the same order was silently skipped as a "duplicate" of the + /// first - or, at preview time, incorrectly flagged "already imported" just because a sibling item from + /// the same order had been. The ASIN is Amazon's own stable per-product id, already parsed from every + /// row, so it's a precise disambiguator - unlike the product title, which is user-editable before commit. + /// + public static string FormatOrderReference(string orderId, string asin) => $"Amazon order {orderId} (ASIN {asin})"; + + /// + /// Reference-data linking is expected to overwrite the created item's title (and, for a book, its ISBN) + /// with the provider's canonical values, and the user may have already cleaned up the title before + /// commit - so this is the one place Amazon's own original listing text is preserved, for an item + /// created by this import. Only used at creation time: a pre-existing item's provenance isn't this + /// import's to invent. is null for every domain but Book. + /// + public static string BuildAmazonProvenanceNotes(string amazonTitle, string? isbn) + { + var lines = new List { $"Title from Amazon: {amazonTitle}" }; + if (isbn is not null) lines.Add($"ISBN from Amazon: {isbn}"); + return string.Join('\n', lines); + } +} diff --git a/src/Domain/Services/AmazonOrderPreviewService.cs b/src/Domain/Services/AmazonOrderPreviewService.cs new file mode 100644 index 00000000..23bce9b9 --- /dev/null +++ b/src/Domain/Services/AmazonOrderPreviewService.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using CsvHelper; +using CsvHelper.Configuration; +using CsvHelper.Configuration.Attributes; +using Keeptrack.Domain.Models; + +namespace Keeptrack.Domain.Services; + +/// +/// Parses an Amazon.fr order-history export ("Request My Data" -> "Your Orders") into review rows. Pure: +/// bytes in, rows out, no repository access - alreadyImportedReferences below is computed by the +/// caller from already-fetched data so this stays testable without a database. +/// Amazon's export has no category column at all, so this never decides "is this a book" on its own - it +/// only computes as a checksum-verified suggestion for +/// the review UI's default filter, confirmed against a real export (an Amazon book's ASIN is routinely its +/// ISBN-10). The user makes the actual call. +/// +public static class AmazonOrderPreviewService +{ + private sealed class AmazonOrderRecord + { + [Name("ASIN")] + public required string Asin { get; set; } + + [Name("Order Date")] + public required string OrderDate { get; set; } + + [Name("Order ID")] + public required string OrderId { get; set; } + + [Name("Product Name")] + public required string ProductName { get; set; } + + [Name("Product Condition")] + public string? ProductCondition { get; set; } + + [Name("Total Amount")] + public string? TotalAmount { get; set; } + + [Name("Website")] + public string? Website { get; set; } + } + + private static readonly CsvConfiguration s_csvConfiguration = new(CultureInfo.InvariantCulture) + { + PrepareHeaderForMatch = args => args.Header.Trim().ToLowerInvariant() + }; + + public static List BuildPreview(Stream csvStream, IReadOnlySet alreadyImportedReferences) + { + using var reader = new StreamReader(csvStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); + using var csv = new CsvReader(reader, s_csvConfiguration); + var records = csv.GetRecords().ToList(); + + return records.Select(record => + { + var suggestedIsbn = IsValidIsbn10(record.Asin) ? record.Asin : null; + + return new AmazonOrderPreviewRow + { + RowId = $"{record.OrderId}:{record.Asin}", + // Some product names also carry a raw numeric HTML entity instead of the character itself + // (confirmed in a real export: "Le Dernier Vœu" instead of "Le Dernier Vœu"). Decoded + // after the mojibake repair, not before - the entity text is plain ASCII, so it round-trips + // through that repair unchanged regardless of order, but repairing first keeps the two + // independent fixes from interacting in either direction. + Title = WebUtility.HtmlDecode(FixMojibake(record.ProductName)), + Asin = record.Asin, + OrderId = record.OrderId, + OrderDate = ParseOrderDate(record.OrderDate), + Price = ParsePrice(record.TotalAmount), + Vendor = record.Website ?? string.Empty, + Condition = record.ProductCondition, + LooksLikeBook = suggestedIsbn is not null, + SuggestedIsbn = suggestedIsbn, + // Per (order, ASIN), not per order - an order commonly has several different line items, and + // an order-id-only check would incorrectly flag every sibling item once any one of them had + // actually been imported (see AmazonImportMergeService.FormatOrderReference). + AlreadyImported = alreadyImportedReferences.Contains(AmazonImportMergeService.FormatOrderReference(record.OrderId, record.Asin)) + }; + }).ToList(); + } + + /// + /// Amazon's export is mojibake for accented text: UTF-8 bytes were re-encoded as if they were Latin-1 + /// (confirmed against the raw bytes of a real export - "protection d'écrans" reads back as "protection + /// d'écrans"). Reinterpreting the decoded chars as Latin-1 bytes and re-decoding as UTF-8 repairs it. + /// The repaired text is only used when it contains no U+FFFD replacement character, so already-correct + /// text (a lone, validly-encoded accented char has no valid single-byte UTF-8 reading) round-trips + /// untouched instead of being corrupted a second time. + /// + private static string FixMojibake(string text) + { + if (text.Length == 0 || text.Any(c => c > 255)) return text; + + var bytes = new byte[text.Length]; + for (var i = 0; i < text.Length; i++) + { + bytes[i] = (byte)text[i]; + } + + var repaired = Encoding.UTF8.GetString(bytes); + return repaired.Contains('�') ? text : repaired; + } + + /// + /// Strips Amazon's Excel-formula-injection-prevention leading apostrophe (confirmed in a real export's + /// "Total Discounts" column, e.g. the literal text '-5') before parsing. + /// + private static decimal? ParsePrice(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) return null; + + var cleaned = raw.Trim().Trim('\''); + return decimal.TryParse(cleaned, NumberStyles.Number, CultureInfo.InvariantCulture, out var value) ? value : null; + } + + private static DateOnly? ParseOrderDate(string raw) => + DateTimeOffset.TryParse(raw, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed) + ? DateOnly.FromDateTime(parsed.UtcDateTime) + : null; + + /// + /// ISBN-10 checksum: 10 digits (last one may be 'X'/'x' meaning check value 10), weighted sum over + /// position (10 down to 1) divisible by 11. Amazon's book ASINs are routinely a real ISBN-10, so this + /// is a strong, false-positive-resistant "is this a book" signal - unlike a bare "10 characters" check. + /// + private static bool IsValidIsbn10(string asin) + { + if (asin.Length != 10) return false; + + var sum = 0; + for (var i = 0; i < 9; i++) + { + if (!char.IsDigit(asin[i])) return false; + sum += (asin[i] - '0') * (10 - i); + } + + var last = asin[9]; + int checkDigit; + if (last is 'X' or 'x') checkDigit = 10; + else if (char.IsDigit(last)) checkDigit = last - '0'; + else return false; + + sum += checkDigit; + return sum % 11 == 0; + } +} diff --git a/src/Domain/Services/GenericVideoGameImportService.cs b/src/Domain/Services/GenericVideoGameImportService.cs new file mode 100644 index 00000000..7b314d8e --- /dev/null +++ b/src/Domain/Services/GenericVideoGameImportService.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using CsvHelper; +using CsvHelper.Configuration; +using CsvHelper.Configuration.Attributes; +using Keeptrack.Domain.Models; + +namespace Keeptrack.Domain.Services; + +/// +/// Parses a generic video game transaction-history CSV (a store's purchase history export - PSN's own GDPR +/// export today, reshaped by the user into this shape; any store exporting the same columns would work too, +/// since is a per-row column rather than something hardcoded here) into review rows. +/// Pure: bytes in, rows out, no repository access - alreadyImportedReferences below is computed by +/// the caller from already-fetched data so this stays testable without a database. Every row is a video +/// game (unlike the Amazon import, which mixes several media types and needs a per-row type guess) - the +/// export is store-purchase history for games specifically. +/// +public static class GenericVideoGameImportService +{ + private sealed class VideoGameTransactionRecord + { + [Name("Transaction Date")] + public required string TransactionDate { get; set; } + + [Name("Game Name")] + public required string GameName { get; set; } + + [Name("Product Name")] + public string? ProductName { get; set; } + + [Name("Platform")] + public required string Platform { get; set; } + + [Name("Vendor")] + public required string Vendor { get; set; } + + [Name("Transaction Id")] + public required string TransactionId { get; set; } + + [Name("Order Id")] + public required string OrderId { get; set; } + + [Name("Final Price (€)")] + public string? FinalPrice { get; set; } + } + + private static readonly CsvConfiguration s_csvConfiguration = new(CultureInfo.InvariantCulture) + { + PrepareHeaderForMatch = args => args.Header.Trim().ToLowerInvariant() + }; + + public static List BuildPreview(Stream csvStream, IReadOnlySet alreadyImportedReferences) + { + using var reader = new StreamReader(csvStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); + using var csv = new CsvReader(reader, s_csvConfiguration); + var records = csv.GetRecords().ToList(); + + return records.Select(record => new GenericVideoGameImportPreviewRow + { + // Transaction Id alone isn't unique per row (see FormatReference's doc comment - a bundled + // transaction has several lines sharing one), so RowId needs the same product disambiguator, + // same shape as AmazonOrderPreviewRow.RowId's "{OrderId}:{Asin}". + RowId = $"{record.TransactionId}:{record.ProductName ?? record.GameName}", + Title = CleanTitle(record.GameName, record.Platform), + Platform = record.Platform, + ProductName = record.ProductName, + Vendor = record.Vendor, + TransactionId = record.TransactionId, + OrderId = record.OrderId, + TransactionDate = ParseTransactionDate(record.TransactionDate), + Price = ParsePrice(record.FinalPrice), + AlreadyImported = alreadyImportedReferences.Contains(FormatReference(record.TransactionId, record.OrderId, record.ProductName, record.GameName)) + }).ToList(); + } + + /// + /// Strips a trailing " ({platform})" suffix from the game title (e.g. "Grand Theft Auto V (PS4)" with + /// platform "PS4" becomes "Grand Theft Auto V") - the export's own title column routinely repeats the + /// platform, redundant now that is its own + /// column. Falls back to the raw title unchanged when the suffix isn't present, so a source that doesn't + /// follow this convention is left alone rather than mangled. + /// + public static string CleanTitle(string gameName, string platform) + { + var suffix = $" ({platform})"; + return gameName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase) ? gameName[..^suffix.Length] : gameName; + } + + /// + /// The one place that formats an owned copy's Reference for an imported transaction line - + /// human-readable, and also the exact-match dedup key + /// looks for on a later + /// re-import. No vendor name here - already carries that. + /// + /// Appends (falling back to when blank) + /// because the transaction id + order id pair is *not* reliably unique per line: a single transaction + /// commonly bundles several different products together (confirmed against a real PSN export - one + /// order/transaction contained three separate "Far Cry 4" DLC packs, each its own line with a distinct + /// Product Name but the exact same Transaction Id/Order Id). Without this, two of those three lines' + /// references collided and were wrongly treated as re-imports of each other - the same class of bug + /// already avoids by including the ASIN + /// alongside the order id, since Amazon's export has a real stable per-product id to use for that; + /// this export has no such id, so the product/edition text is the next best disambiguator. + /// + public static string FormatReference(string transactionId, string orderId, string? productName, string fallbackTitle) + { + var descriptor = string.IsNullOrWhiteSpace(productName) ? fallbackTitle : productName; + return $"Order {orderId} (transaction {transactionId}) - {descriptor}"; + } + + /// + /// Reference-data linking is expected to overwrite the created item's title with the provider's + /// canonical value, and the user may have already cleaned up the title before commit - so this is the + /// one place the source export's original listing text is preserved, for an item created by this + /// import. Only used at creation time: a pre-existing item's provenance isn't this import's to invent. + /// + public static string BuildProvenanceNotes(string vendor, string sourceTitle) => $"Title from {vendor}: {sourceTitle}"; + + private static decimal? ParsePrice(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) return null; + + return decimal.TryParse(raw.Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out var value) ? value : null; + } + + private static DateOnly? ParseTransactionDate(string raw) => + DateTimeOffset.TryParse(raw, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed) + ? DateOnly.FromDateTime(parsed.UtcDateTime) + : null; +} diff --git a/src/Domain/Services/OwnedItemImportMergeService.cs b/src/Domain/Services/OwnedItemImportMergeService.cs new file mode 100644 index 00000000..89f70485 --- /dev/null +++ b/src/Domain/Services/OwnedItemImportMergeService.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Keeptrack.Common.System; +using Keeptrack.Domain.Models; + +namespace Keeptrack.Domain.Services; + +/// +/// Computes what to create/update when committing a set of user-reviewed import rows, for any trackable item +/// type. Pure: takes the owner's already-fetched items and the selected rows, returns a plan - all +/// repository access stays in the calling controller. Matching is by normalized title +/// (, the same "same title" rule the TV Time import already established), +/// merging within the same commit batch too: two selected rows sharing a title become one item with two +/// owned copies, even if neither existed before this commit. +/// +/// Generic over both the tracked model (BookModel/MovieModel/TvShowModel/VideoGameModel) +/// and its own request-item shape via delegates rather than a shared interface - Book/Movie/TvShow's +/// OwnedVersions and VideoGame's differently-shaped Platforms both flow through the exact same +/// algorithm this way, with no changes to any of those models. Extracted out of the originally Amazon-only +/// once a second importer (the generic video game transaction import) +/// needed the exact same engine - the two members here were already fully generic (no Amazon-specific +/// concept anywhere in their bodies), only and +/// are genuinely Amazon-specific and stayed +/// behind. +/// +public static class OwnedItemImportMergeService +{ + /// + /// Every reference already recorded on an existing owned copy - used at preview time to flag rows that + /// look like they were imported before, so re-uploading a newer export doesn't duplicate them. Compared + /// by exact string equality against whatever reference-formatting the calling importer uses for its own + /// candidate rows (e.g. ), which is what makes + /// per-line-item (not per-order) precision actually take effect. reads + /// whichever collection carries the owned copies for (OwnedVersions + /// or Platforms). + /// + public static HashSet FindImportedReferences(IEnumerable existingItems, Func> getReferences) => + existingItems.SelectMany(getReferences).Where(reference => reference is not null).ToHashSet()!; + + /// + /// / make the caller's own + /// reference (see ) the *primary* dedup key, not just an + /// advisory preview-time flag: a re-committed row whose reference already exists on some existing item - + /// under any title, even one reference-data linking has since renamed - is skipped outright rather than + /// falling through to title matching. Two real bugs (found on the original Amazon import) motivated this: + /// (1) re-running the same commit created a second owned copy for the same order every time, because + /// title-matching alone doesn't know "this exact copy is already here"; (2) once an item's title changed + /// after a first import, title-matching a re-import of the same order no longer found it at all and + /// created a brand new duplicate item instead. Title matching is still the fallback for a genuinely new + /// order of an item already owned under a different order (a second copy bought separately). + /// + public static ImportCommitPlan ComputeCommitPlan( + IReadOnlyCollection existingItems, + IReadOnlyList items, + Func getExistingTitle, + Func> getExistingReferences, + Func getItemTitle, + Func getItemReference, + Func createNew, + Action appendOwnedCopy) + where TModel : class + { + var plan = new ImportCommitPlan(); + + var byNormalizedTitle = new Dictionary(); + var byReference = new Dictionary(); + IndexExistingItems(existingItems, getExistingTitle, getExistingReferences, byNormalizedTitle, byReference); + + // Items created earlier in this same batch are tracked separately from pre-existing ones: a later + // row matching one must only get its owned copy appended (already reflected via ItemsToCreate), + // never also queued onto ItemsToUpdate - it has no Id yet, so "updating" it would be meaningless. + var createdThisBatch = new HashSet(); + + foreach (var item in items) + { + MergeItem(item, plan, byNormalizedTitle, byReference, createdThisBatch, getItemTitle, getItemReference, createNew, appendOwnedCopy); + } + + return plan; + } + + private static void IndexExistingItems( + IReadOnlyCollection existingItems, + Func getExistingTitle, + Func> getExistingReferences, + Dictionary byNormalizedTitle, + Dictionary byReference) + where TModel : class + { + foreach (var existing in existingItems) + { + byNormalizedTitle.TryAdd(TitleNormalizer.Normalize(getExistingTitle(existing)), existing); + foreach (var reference in getExistingReferences(existing).OfType()) + { + byReference.TryAdd(reference, existing); + } + } + } + + private static void MergeItem( + TRequestItem item, + ImportCommitPlan plan, + Dictionary byNormalizedTitle, + Dictionary byReference, + HashSet createdThisBatch, + Func getItemTitle, + Func getItemReference, + Func createNew, + Action appendOwnedCopy) + where TModel : class + { + var reference = getItemReference(item); + if (reference is not null && byReference.ContainsKey(reference)) + { + plan.OwnedCopiesSkipped++; + plan.SkippedTitles.Add(getItemTitle(item)); + return; + } + + var key = TitleNormalizer.Normalize(getItemTitle(item)); + TModel target; + + if (byNormalizedTitle.TryGetValue(key, out var existing)) + { + appendOwnedCopy(existing, item); + if (!createdThisBatch.Contains(existing) && !plan.ItemsToUpdate.Contains(existing)) + { + plan.ItemsToUpdate.Add(existing); + } + + target = existing; + } + else + { + var created = createNew(item); + plan.ItemsToCreate.Add(created); + createdThisBatch.Add(created); + + // so a later row in the same batch sharing this title merges into it too, instead of + // creating a second item + byNormalizedTitle[key] = created; + target = created; + } + + // registers this reference immediately (not just from the initial existingItems scan), so a + // second row in the same batch carrying the same reference is caught by the check above too + if (reference is not null) byReference[reference] = target; + + plan.OwnedCopiesAdded++; + } +} diff --git a/src/Infrastructure.MongoDb/Entities/Album.cs b/src/Infrastructure.MongoDb/Entities/Album.cs index bef1f3ec..3306fe63 100644 --- a/src/Infrastructure.MongoDb/Entities/Album.cs +++ b/src/Infrastructure.MongoDb/Entities/Album.cs @@ -27,6 +27,9 @@ public class Album : IHasIdAndOwnerId [BsonElement("reference_id")] public string? ReferenceId { get; set; } + [BsonElement("custom_image_url")] + public string? CustomImageUrl { get; set; } + [BsonElement("is_favorite")] public bool IsFavorite { get; set; } diff --git a/src/Infrastructure.MongoDb/Entities/Car.cs b/src/Infrastructure.MongoDb/Entities/Car.cs index 246bc6de..8fb3603a 100644 --- a/src/Infrastructure.MongoDb/Entities/Car.cs +++ b/src/Infrastructure.MongoDb/Entities/Car.cs @@ -28,4 +28,7 @@ public class Car : IHasIdAndOwnerId [BsonElement("energy_type")] public required CarEnergyType EnergyType { get; set; } + + [BsonElement("image_url")] + public string? ImageUrl { get; set; } } diff --git a/src/Infrastructure.MongoDb/Entities/Collectible.cs b/src/Infrastructure.MongoDb/Entities/Collectible.cs new file mode 100644 index 00000000..71d97c0f --- /dev/null +++ b/src/Infrastructure.MongoDb/Entities/Collectible.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Keeptrack.Common.System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Keeptrack.Infrastructure.MongoDb.Entities; + +public class Collectible : IHasIdAndOwnerId +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string? Id { get; set; } + + [BsonElement("owner_id")] + public required string OwnerId { get; set; } + + public required string Title { get; set; } + + public string? Brand { get; set; } + + public int? Year { get; set; } + + public string? Notes { get; set; } + + [BsonElement("image_url")] + public string? ImageUrl { get; set; } + + [BsonElement("is_favorite")] + public bool IsFavorite { get; set; } + + [BsonElement("owned_versions")] + public List OwnedVersions { get; set; } = []; +} diff --git a/src/Infrastructure.MongoDb/Entities/Gear.cs b/src/Infrastructure.MongoDb/Entities/Gear.cs new file mode 100644 index 00000000..2f3b2193 --- /dev/null +++ b/src/Infrastructure.MongoDb/Entities/Gear.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Keeptrack.Common.System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Keeptrack.Infrastructure.MongoDb.Entities; + +public class Gear : IHasIdAndOwnerId +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string? Id { get; set; } + + [BsonElement("owner_id")] + public required string OwnerId { get; set; } + + public required string Title { get; set; } + + public string? Brand { get; set; } + + public string? Category { get; set; } + + public int? Year { get; set; } + + public string? Notes { get; set; } + + [BsonElement("image_url")] + public string? ImageUrl { get; set; } + + [BsonElement("is_favorite")] + public bool IsFavorite { get; set; } + + [BsonElement("owned_versions")] + public List OwnedVersions { get; set; } = []; +} diff --git a/src/Infrastructure.MongoDb/Entities/HealthProfile.cs b/src/Infrastructure.MongoDb/Entities/HealthProfile.cs index ac47fcfc..06f545f5 100644 --- a/src/Infrastructure.MongoDb/Entities/HealthProfile.cs +++ b/src/Infrastructure.MongoDb/Entities/HealthProfile.cs @@ -16,4 +16,7 @@ public class HealthProfile : IHasIdAndOwnerId public required string Name { get; set; } public string? Notes { get; set; } + + [BsonElement("image_url")] + public string? ImageUrl { get; set; } } diff --git a/src/Infrastructure.MongoDb/Entities/House.cs b/src/Infrastructure.MongoDb/Entities/House.cs index 9f0322f1..181e4a37 100644 --- a/src/Infrastructure.MongoDb/Entities/House.cs +++ b/src/Infrastructure.MongoDb/Entities/House.cs @@ -29,4 +29,7 @@ public class House : IHasIdAndOwnerId public DateTime? MovedOutAt { get; set; } public string? Notes { get; set; } + + [BsonElement("image_url")] + public string? ImageUrl { get; set; } } diff --git a/src/Infrastructure.MongoDb/Entities/OwnedVersion.cs b/src/Infrastructure.MongoDb/Entities/OwnedVersion.cs index 5ddcd06e..7d38b462 100644 --- a/src/Infrastructure.MongoDb/Entities/OwnedVersion.cs +++ b/src/Infrastructure.MongoDb/Entities/OwnedVersion.cs @@ -19,4 +19,7 @@ public class OwnedVersion public DateTime? AcquiredAt { get; set; } public string? Reference { get; set; } + + [BsonElement("product_name")] + public string? ProductName { get; set; } } diff --git a/src/Infrastructure.MongoDb/Entities/UserPreferences.cs b/src/Infrastructure.MongoDb/Entities/UserPreferences.cs new file mode 100644 index 00000000..00dbd811 --- /dev/null +++ b/src/Infrastructure.MongoDb/Entities/UserPreferences.cs @@ -0,0 +1,18 @@ +using Keeptrack.Common.System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Keeptrack.Infrastructure.MongoDb.Entities; + +public class UserPreferences : IHasIdAndOwnerId +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string? Id { get; set; } + + [BsonElement("owner_id")] + public required string OwnerId { get; set; } + + [BsonElement("features")] + public UserPreferencesFeatures Features { get; set; } = new(); +} diff --git a/src/Infrastructure.MongoDb/Entities/UserPreferencesFeatures.cs b/src/Infrastructure.MongoDb/Entities/UserPreferencesFeatures.cs new file mode 100644 index 00000000..179fe177 --- /dev/null +++ b/src/Infrastructure.MongoDb/Entities/UserPreferencesFeatures.cs @@ -0,0 +1,12 @@ +using MongoDB.Bson.Serialization.Attributes; + +namespace Keeptrack.Infrastructure.MongoDb.Entities; + +public class UserPreferencesFeatures +{ + [BsonElement("show_chasse_aux_livres_link")] + public bool ShowChasseAuxLivresLink { get; set; } + + [BsonElement("show_amazon_product_link")] + public bool ShowAmazonProductLink { get; set; } +} diff --git a/src/Infrastructure.MongoDb/Entities/VideoGame.cs b/src/Infrastructure.MongoDb/Entities/VideoGame.cs index bbe239da..66b3c483 100644 --- a/src/Infrastructure.MongoDb/Entities/VideoGame.cs +++ b/src/Infrastructure.MongoDb/Entities/VideoGame.cs @@ -27,6 +27,9 @@ public class VideoGame : IHasIdAndOwnerId [BsonElement("reference_id")] public string? ReferenceId { get; set; } + [BsonElement("custom_image_url")] + public string? CustomImageUrl { get; set; } + [BsonElement("is_wishlisted")] public bool IsWishlisted { get; set; } } diff --git a/src/Infrastructure.MongoDb/Entities/VideoGamePlatform.cs b/src/Infrastructure.MongoDb/Entities/VideoGamePlatform.cs index c5abe624..ba65eaa3 100644 --- a/src/Infrastructure.MongoDb/Entities/VideoGamePlatform.cs +++ b/src/Infrastructure.MongoDb/Entities/VideoGamePlatform.cs @@ -35,4 +35,7 @@ public class VideoGamePlatform public DateTime? AcquiredAt { get; set; } public string? Reference { get; set; } + + [BsonElement("product_name")] + public string? ProductName { get; set; } } diff --git a/src/Infrastructure.MongoDb/Mappers/CollectibleStorageMapper.cs b/src/Infrastructure.MongoDb/Mappers/CollectibleStorageMapper.cs new file mode 100644 index 00000000..58a19614 --- /dev/null +++ b/src/Infrastructure.MongoDb/Mappers/CollectibleStorageMapper.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Keeptrack.Domain.Models; +using Keeptrack.Infrastructure.MongoDb.Entities; +using Riok.Mapperly.Abstractions; + +namespace Keeptrack.Infrastructure.MongoDb.Mappers; + +[Mapper] +[UseStaticMapper(typeof(CommonStorageMappings))] +public partial class CollectibleStorageMapper : IStorageMapper +{ + // IsOwned is filter-only (derived from OwnedVersions) - see MovieStorageMapper. + [MapperIgnoreSource(nameof(CollectibleModel.IsOwned))] + public partial Collectible ToEntity(CollectibleModel model); + + [MapperIgnoreTarget(nameof(CollectibleModel.IsOwned))] + public partial CollectibleModel ToModel(Collectible entity); + + public partial List ToModels(List entities); +} diff --git a/src/Infrastructure.MongoDb/Mappers/GearStorageMapper.cs b/src/Infrastructure.MongoDb/Mappers/GearStorageMapper.cs new file mode 100644 index 00000000..a4587c42 --- /dev/null +++ b/src/Infrastructure.MongoDb/Mappers/GearStorageMapper.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Keeptrack.Domain.Models; +using Keeptrack.Infrastructure.MongoDb.Entities; +using Riok.Mapperly.Abstractions; + +namespace Keeptrack.Infrastructure.MongoDb.Mappers; + +[Mapper] +[UseStaticMapper(typeof(CommonStorageMappings))] +public partial class GearStorageMapper : IStorageMapper +{ + // IsOwned is filter-only (derived from OwnedVersions) - see MovieStorageMapper. + [MapperIgnoreSource(nameof(GearModel.IsOwned))] + public partial Gear ToEntity(GearModel model); + + [MapperIgnoreTarget(nameof(GearModel.IsOwned))] + public partial GearModel ToModel(Gear entity); + + public partial List ToModels(List entities); +} diff --git a/src/Infrastructure.MongoDb/Mappers/UserPreferencesStorageMapper.cs b/src/Infrastructure.MongoDb/Mappers/UserPreferencesStorageMapper.cs new file mode 100644 index 00000000..e5c5af2f --- /dev/null +++ b/src/Infrastructure.MongoDb/Mappers/UserPreferencesStorageMapper.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Keeptrack.Domain.Models; +using Keeptrack.Infrastructure.MongoDb.Entities; +using Riok.Mapperly.Abstractions; + +namespace Keeptrack.Infrastructure.MongoDb.Mappers; + +[Mapper] +public partial class UserPreferencesStorageMapper : IStorageMapper +{ + public partial UserPreferences ToEntity(UserPreferencesModel model); + + public partial UserPreferencesModel ToModel(UserPreferences entity); + + public partial List ToModels(List entities); +} diff --git a/src/Infrastructure.MongoDb/Repositories/AlbumReferenceRepository.cs b/src/Infrastructure.MongoDb/Repositories/AlbumReferenceRepository.cs index a50fec1b..44b80e2d 100644 --- a/src/Infrastructure.MongoDb/Repositories/AlbumReferenceRepository.cs +++ b/src/Infrastructure.MongoDb/Repositories/AlbumReferenceRepository.cs @@ -88,4 +88,9 @@ public async Task UpsertAsync(AlbumReferenceModel model) return mapper.ToModel(entity); } + + public async Task DeleteAsync(string id) + { + await Collection.DeleteOneAsync(x => x.Id == id); + } } diff --git a/src/Infrastructure.MongoDb/Repositories/BookReferenceRepository.cs b/src/Infrastructure.MongoDb/Repositories/BookReferenceRepository.cs index 82d34690..51502e62 100644 --- a/src/Infrastructure.MongoDb/Repositories/BookReferenceRepository.cs +++ b/src/Infrastructure.MongoDb/Repositories/BookReferenceRepository.cs @@ -90,4 +90,9 @@ public async Task UpsertAsync(BookReferenceModel model) return mapper.ToModel(entity); } + + public async Task DeleteAsync(string id) + { + await Collection.DeleteOneAsync(x => x.Id == id); + } } diff --git a/src/Infrastructure.MongoDb/Repositories/CollectibleRepository.cs b/src/Infrastructure.MongoDb/Repositories/CollectibleRepository.cs new file mode 100644 index 00000000..5dadafba --- /dev/null +++ b/src/Infrastructure.MongoDb/Repositories/CollectibleRepository.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq.Expressions; +using Keeptrack.Domain.Models; +using Keeptrack.Domain.Repositories; +using Keeptrack.Infrastructure.MongoDb.Entities; +using Keeptrack.Infrastructure.MongoDb.Mappers; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; + +namespace Keeptrack.Infrastructure.MongoDb.Repositories; + +public class CollectibleRepository(IMongoDatabase mongoDatabase, ILogger logger, IStorageMapper mapper) + : MongoDbRepositoryBase(mongoDatabase, logger, mapper), ICollectibleRepository +{ + protected override string CollectionName => "collectible"; + + protected override Expression> SortTitleField => x => x.Title; + + protected override FilterDefinition GetFilter(string ownerId, string? search, CollectibleModel input) + { + var builder = Builders.Filter; + var filter = builder.Eq(f => f.OwnerId, ownerId); + if (!string.IsNullOrEmpty(search)) filter &= builder.Where(f => f.Title.Contains(search, StringComparison.CurrentCultureIgnoreCase) + || (f.Brand != null && f.Brand.Contains(search, StringComparison.CurrentCultureIgnoreCase))); + if (input.IsFavorite) filter &= builder.Eq(f => f.IsFavorite, true); + // "owned" means at least one owned version - see MovieRepository.GetFilter + if (input.IsOwned) filter &= builder.SizeGt(f => f.OwnedVersions, 0); + return filter; + } +} diff --git a/src/Infrastructure.MongoDb/Repositories/GearRepository.cs b/src/Infrastructure.MongoDb/Repositories/GearRepository.cs new file mode 100644 index 00000000..fe59ac80 --- /dev/null +++ b/src/Infrastructure.MongoDb/Repositories/GearRepository.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Keeptrack.Common.System; +using Keeptrack.Domain.Models; +using Keeptrack.Domain.Repositories; +using Keeptrack.Infrastructure.MongoDb.Entities; +using Keeptrack.Infrastructure.MongoDb.Mappers; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; + +namespace Keeptrack.Infrastructure.MongoDb.Repositories; + +public class GearRepository(IMongoDatabase mongoDatabase, ILogger logger, IStorageMapper mapper) + : MongoDbRepositoryBase(mongoDatabase, logger, mapper), IGearRepository +{ + protected override string CollectionName => "gear"; + + protected override Expression> SortTitleField => x => x.Title; + + /// + /// "Bought" needs the max AcquiredAt across a gear item's + /// array (the most recently acquired copy), not a single scalar field, so it can't use the shared + /// SortSecondaryDateField hook - same shape as VideoGameRepository.GetSort's "last + /// completed" override, relying on MongoDB's native max-on-descending-sort semantics for a dotted + /// array field rather than an aggregation pipeline. + /// + protected override SortDefinition GetSort(string? sort) => + sort == ListSort.Bought + ? Builders.Sort.Descending("owned_versions.acquired_at").Descending("_id") + : base.GetSort(sort); + + protected override FilterDefinition GetFilter(string ownerId, string? search, GearModel input) + { + var builder = Builders.Filter; + var filter = builder.Eq(f => f.OwnerId, ownerId); + if (!string.IsNullOrEmpty(search)) filter &= builder.Where(f => f.Title.Contains(search, StringComparison.CurrentCultureIgnoreCase) + || (f.Brand != null && f.Brand.Contains(search, StringComparison.CurrentCultureIgnoreCase))); + if (input.IsFavorite) filter &= builder.Eq(f => f.IsFavorite, true); + if (!string.IsNullOrEmpty(input.Category)) filter &= builder.Eq(f => f.Category, input.Category); + // "owned" means at least one owned version - see MovieRepository.GetFilter + if (input.IsOwned) filter &= builder.SizeGt(f => f.OwnedVersions, 0); + return filter; + } + + public async Task> FindDistinctCategoriesAsync(string ownerId) + { + var builder = Builders.Filter; + // "has a category" is the negation of TvShowRepository/MovieRepository's UnresolvedFilter shape + // (matches null OR empty string) - both generations of "unset" must be excluded here too. + var filter = builder.Eq(f => f.OwnerId, ownerId) & builder.Ne(f => f.Category, null) & builder.Ne(f => f.Category, string.Empty); + var cursor = await GetCollection().DistinctAsync(f => f.Category, filter); + var categories = await cursor.ToListAsync(); + categories.Sort(StringComparer.OrdinalIgnoreCase); + return categories!; + } +} diff --git a/src/Infrastructure.MongoDb/Repositories/MovieReferenceRepository.cs b/src/Infrastructure.MongoDb/Repositories/MovieReferenceRepository.cs index be1f86b9..6bb3e274 100644 --- a/src/Infrastructure.MongoDb/Repositories/MovieReferenceRepository.cs +++ b/src/Infrastructure.MongoDb/Repositories/MovieReferenceRepository.cs @@ -91,4 +91,9 @@ public async Task UpsertAsync(MovieReferenceModel model) return mapper.ToModel(entity); } + + public async Task DeleteAsync(string id) + { + await Collection.DeleteOneAsync(x => x.Id == id); + } } diff --git a/src/Infrastructure.MongoDb/Repositories/TvShowReferenceRepository.cs b/src/Infrastructure.MongoDb/Repositories/TvShowReferenceRepository.cs index 54e70882..ac058e26 100644 --- a/src/Infrastructure.MongoDb/Repositories/TvShowReferenceRepository.cs +++ b/src/Infrastructure.MongoDb/Repositories/TvShowReferenceRepository.cs @@ -86,4 +86,9 @@ public async Task UpsertAsync(TvShowReferenceModel model) return mapper.ToModel(entity); } + + public async Task DeleteAsync(string id) + { + await Collection.DeleteOneAsync(x => x.Id == id); + } } diff --git a/src/Infrastructure.MongoDb/Repositories/UserPreferencesRepository.cs b/src/Infrastructure.MongoDb/Repositories/UserPreferencesRepository.cs new file mode 100644 index 00000000..e06706b0 --- /dev/null +++ b/src/Infrastructure.MongoDb/Repositories/UserPreferencesRepository.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using Keeptrack.Domain.Models; +using Keeptrack.Domain.Repositories; +using Keeptrack.Infrastructure.MongoDb.Entities; +using Keeptrack.Infrastructure.MongoDb.Mappers; +using MongoDB.Driver; + +namespace Keeptrack.Infrastructure.MongoDb.Repositories; + +public class UserPreferencesRepository(IMongoDatabase mongoDatabase, UserPreferencesStorageMapper mapper) : IUserPreferencesRepository +{ + private const string CollectionName = "user_preference"; + + private IMongoCollection Collection => mongoDatabase.GetCollection(CollectionName); + + public async Task FindByOwnerIdAsync(string ownerId) + { + var entity = await Collection.Find(x => x.OwnerId == ownerId).FirstOrDefaultAsync(); + // the usual null guard before mapping - see MongoDbRepositoryBase.FindOneAsync's identical shape + return entity is null ? null : mapper.ToModel(entity); + } + + public async Task UpsertAsync(UserPreferencesModel model) + { + var entity = mapper.ToEntity(model); + await Collection.ReplaceOneAsync(x => x.OwnerId == model.OwnerId, entity, new ReplaceOptions { IsUpsert = true }); + } +} diff --git a/src/Infrastructure.MongoDb/Repositories/VideoGameReferenceRepository.cs b/src/Infrastructure.MongoDb/Repositories/VideoGameReferenceRepository.cs index af6278bd..0d71b2a9 100644 --- a/src/Infrastructure.MongoDb/Repositories/VideoGameReferenceRepository.cs +++ b/src/Infrastructure.MongoDb/Repositories/VideoGameReferenceRepository.cs @@ -80,4 +80,9 @@ public async Task UpsertAsync(VideoGameReferenceModel m return mapper.ToModel(entity); } + + public async Task DeleteAsync(string id) + { + await Collection.DeleteOneAsync(x => x.Id == id); + } } diff --git a/src/WebApi.Contracts/Dto/AlbumDto.cs b/src/WebApi.Contracts/Dto/AlbumDto.cs index f8a3e929..882de0cf 100644 --- a/src/WebApi.Contracts/Dto/AlbumDto.cs +++ b/src/WebApi.Contracts/Dto/AlbumDto.cs @@ -46,11 +46,18 @@ public class AlbumDto : IHasId, IReferenceLinkedDto public string? ReferenceId { get; set; } /// - /// Cover/poster image URL from the linked reference document - read-only, hydrated server-side on - /// list reads and never accepted from client input. + /// Cover image URL shown on the list page - when set, otherwise the linked + /// reference document's own cover. Read-only, hydrated server-side on list reads; never accepted from + /// client input (edit instead). /// public string? ImageUrl { get; set; } + /// + /// Tenant-owned cover image override, freely editable - takes priority over the linked reference's + /// cover wherever one is shown. Null means "use the reference's cover, if any". + /// + public string? CustomImageUrl { get; set; } + public bool IsFavorite { get; set; } /// diff --git a/src/WebApi.Contracts/Dto/AmazonImportCommitItemDto.cs b/src/WebApi.Contracts/Dto/AmazonImportCommitItemDto.cs new file mode 100644 index 00000000..6d54e891 --- /dev/null +++ b/src/WebApi.Contracts/Dto/AmazonImportCommitItemDto.cs @@ -0,0 +1,68 @@ +using System; + +namespace Keeptrack.WebApi.Contracts.Dto; + +/// +/// One row selected for import, carrying whatever the user edited in the review table. +/// +public class AmazonImportCommitItemDto +{ + /// + /// The this came from. Not used for anything server-side + /// beyond echoing it back in error messages - the row's data is taken entirely from this DTO's own fields. + /// + public required string RowId { get; set; } + + /// + /// Amazon's own product id for this order line and the order number itself - together they're the + /// server-derived owned-copy Reference (see AmazonImportMergeService.FormatOrderReference + /// in the WebApi project), which also doubles as the exact dedup key. Both are echoed back from the + /// preview row rather than accepting a client-supplied Reference string directly, so the format + /// can't drift out of sync with what a later re-preview checks against, and so a single order's several + /// different line items (same order id, different ASIN) are never conflated with one another. + /// + public required string OrderId { get; set; } + + public required string Asin { get; set; } + + public required string Title { get; set; } + + /// + /// The title exactly as Amazon listed it, even if was edited in the review UI - + /// recorded in the created item's notes, since reference-data linking is expected to overwrite + /// later. + /// + public required string AmazonTitle { get; set; } + + /// + /// Which trackable item type to create/merge this row as. Nullable (and validated as required + /// server-side, same as for a video game) rather than defaulting to + /// , so a row the "looks like a book" heuristic didn't suggest + /// can't be silently committed as a book just because the reviewer forgot to pick a type. + /// + public AmazonImportMediaType? MediaType { get; set; } + + /// + /// Publication/release year, if the user happens to know it - Amazon's export has no source for this + /// (order date is the purchase year, not the item's), so it's never auto-filled. + /// + public int? Year { get; set; } + + /// Book-only - ignored for every other . + public string? Isbn { get; set; } + + /// + /// VideoGame-only (PS5/Xbox/PC/Switch...) - required and validated server-side when + /// is , ignored otherwise. + /// Amazon's export has no signal for this at all. + /// + public string? Platform { get; set; } + + public DateOnly? AcquiredAt { get; set; } + + public decimal? Price { get; set; } + + public string? Vendor { get; set; } + + public CopyType CopyType { get; set; } +} diff --git a/src/WebApi.Contracts/Dto/AmazonImportCommitRequestDto.cs b/src/WebApi.Contracts/Dto/AmazonImportCommitRequestDto.cs new file mode 100644 index 00000000..cd709c35 --- /dev/null +++ b/src/WebApi.Contracts/Dto/AmazonImportCommitRequestDto.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Keeptrack.WebApi.Contracts.Dto; + +/// +/// The set of Amazon order-history rows the user picked in the review UI, ready to be created as books. +/// +public class AmazonImportCommitRequestDto +{ + public required List Items { get; set; } +} diff --git a/src/WebApi.Contracts/Dto/AmazonImportCommitResultDto.cs b/src/WebApi.Contracts/Dto/AmazonImportCommitResultDto.cs new file mode 100644 index 00000000..03978605 --- /dev/null +++ b/src/WebApi.Contracts/Dto/AmazonImportCommitResultDto.cs @@ -0,0 +1,62 @@ +namespace Keeptrack.WebApi.Contracts.Dto; + +/// +/// Outcome of committing a selected set of Amazon order rows, broken down per trackable item type - only +/// the types actually present in the commit request end up non-zero. +/// +public class AmazonImportCommitResultDto +{ + /// Brand new books created. + public int BooksCreated { get; set; } + + /// Existing books (including ones created earlier in this same commit) that received an additional owned version. + public int BooksMergedInto { get; set; } + + /// Rows whose order reference already matched an existing owned version - not duplicated. + public int BooksSkipped { get; set; } + + /// Brand new movies created. + public int MoviesCreated { get; set; } + + /// Existing movies (including ones created earlier in this same commit) that received an additional owned version. + public int MoviesMergedInto { get; set; } + + /// Rows whose order reference already matched an existing owned version - not duplicated. + public int MoviesSkipped { get; set; } + + /// Brand new TV shows created. + public int TvShowsCreated { get; set; } + + /// Existing TV shows (including ones created earlier in this same commit) that received an additional owned version. + public int TvShowsMergedInto { get; set; } + + /// Rows whose order reference already matched an existing owned version - not duplicated. + public int TvShowsSkipped { get; set; } + + /// Brand new video games created. + public int VideoGamesCreated { get; set; } + + /// Existing video games (including ones created earlier in this same commit) that received an additional platform entry. + public int VideoGamesMergedInto { get; set; } + + /// Rows whose order reference already matched an existing platform entry - not duplicated. + public int VideoGamesSkipped { get; set; } + + /// Brand new gear items created. + public int GearCreated { get; set; } + + /// Existing gear items (including ones created earlier in this same commit) that received an additional owned version. + public int GearMergedInto { get; set; } + + /// Rows whose order reference already matched an existing owned version - not duplicated. + public int GearSkipped { get; set; } + + /// Brand new collectibles created. + public int CollectiblesCreated { get; set; } + + /// Existing collectibles (including ones created earlier in this same commit) that received an additional owned version. + public int CollectiblesMergedInto { get; set; } + + /// Rows whose order reference already matched an existing owned version - not duplicated. + public int CollectiblesSkipped { get; set; } +} diff --git a/src/WebApi.Contracts/Dto/AmazonImportMediaType.cs b/src/WebApi.Contracts/Dto/AmazonImportMediaType.cs new file mode 100644 index 00000000..609d2691 --- /dev/null +++ b/src/WebApi.Contracts/Dto/AmazonImportMediaType.cs @@ -0,0 +1,15 @@ +namespace Keeptrack.WebApi.Contracts.Dto; + +/// +/// Which trackable item type an Amazon order-history row should be imported as - picked per row in the +/// review UI, since Amazon's export has no category column to detect this automatically. +/// +public enum AmazonImportMediaType +{ + Book, + Movie, + TvShow, + VideoGame, + Gear, + Collectible +} diff --git a/src/WebApi.Contracts/Dto/AmazonOrderPreviewRowDto.cs b/src/WebApi.Contracts/Dto/AmazonOrderPreviewRowDto.cs new file mode 100644 index 00000000..78b711c4 --- /dev/null +++ b/src/WebApi.Contracts/Dto/AmazonOrderPreviewRowDto.cs @@ -0,0 +1,63 @@ +using System; + +namespace Keeptrack.WebApi.Contracts.Dto; + +/// +/// One line item parsed from an uploaded Amazon order-history export, awaiting the user's review before +/// anything is imported. See for what gets sent back once selected. +/// +public class AmazonOrderPreviewRowDto +{ + /// + /// Correlates a selected/edited row back to this one at commit time. Stable within one export, never stored. + /// + public required string RowId { get; set; } + + /// + /// Amazon's product name, with the export's mojibake repaired. + /// + public required string Title { get; set; } + + /// + /// Amazon's own product identifier for this order line. + /// + public required string Asin { get; set; } + + /// + /// Amazon's order number. + /// + public required string OrderId { get; set; } + + public DateOnly? OrderDate { get; set; } + + /// + /// What was actually paid for this item - tax and any per-item discount already netted in. + /// + public decimal? Price { get; set; } + + /// + /// The storefront the order was placed on (e.g. "Amazon.fr"). + /// + public required string Vendor { get; set; } + + /// + /// Display only ("New", "Used"...) - not carried onto the created book. + /// + public string? Condition { get; set; } + + /// + /// True when passes an ISBN-10 checksum - the review table's default filter. + /// + public bool LooksLikeBook { get; set; } + + /// + /// again, only when is true. + /// + public string? SuggestedIsbn { get; set; } + + /// + /// True when an existing book already has an owned version referencing this order. Defaults to + /// unchecked in the review UI, but stays editable/selectable in case the user wants to re-import anyway. + /// + public bool AlreadyImported { get; set; } +} diff --git a/src/WebApi.Contracts/Dto/CarDto.cs b/src/WebApi.Contracts/Dto/CarDto.cs index 1d9fb71a..996fe9cf 100644 --- a/src/WebApi.Contracts/Dto/CarDto.cs +++ b/src/WebApi.Contracts/Dto/CarDto.cs @@ -41,4 +41,9 @@ public class CarDto : IHasId /// Energy type (Combustion, Hybrid, Electric). /// public CarEnergyType? EnergyType { get; set; } + + /// + /// Tenant-owned cover image URL, shown on the list thumbnail and detail page header. Optional. + /// + public string? ImageUrl { get; set; } } diff --git a/src/WebApi.Contracts/Dto/CollectibleDto.cs b/src/WebApi.Contracts/Dto/CollectibleDto.cs new file mode 100644 index 00000000..93ec07ca --- /dev/null +++ b/src/WebApi.Contracts/Dto/CollectibleDto.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using Keeptrack.Common.System; + +namespace Keeptrack.WebApi.Contracts.Dto; + +/// +/// A collectible item the user owns (e.g. a Lego set, a figurine). +/// +public class CollectibleDto : IHasId +{ + /// + /// Unique identifier. + /// + public string? Id { get; set; } + + /// + /// Collectible name. + /// + /// Millennium Falcon + public string? Title { get; set; } + + /// + /// Brand or manufacturer. + /// + /// Lego + public string? Brand { get; set; } + + /// + /// Year. + /// + public int? Year { get; set; } + + /// + /// Free-text notes (where it's stored, where it's displayed, or any other specifics). + /// + public string? Notes { get; set; } + + /// + /// Tenant-owned cover image URL, shown on the list thumbnail and detail page header. Optional. + /// + public string? ImageUrl { get; set; } + + public bool IsFavorite { get; set; } + + /// + /// Every owned copy of this item - it counts as owned when this list is non-empty. + /// + public List OwnedVersions { get; set; } = []; + + /// + /// Filter-only query parameter: matches items with at least one owned version. Never populated on a + /// returned item - see for the convention. + /// + public bool IsOwned { get; set; } +} diff --git a/src/WebApi.Contracts/Dto/CollectionStatsDto.cs b/src/WebApi.Contracts/Dto/CollectionStatsDto.cs index d53e2ab4..3220c33c 100644 --- a/src/WebApi.Contracts/Dto/CollectionStatsDto.cs +++ b/src/WebApi.Contracts/Dto/CollectionStatsDto.cs @@ -31,4 +31,10 @@ public class CollectionStatsDto /// Number of houses. public long Houses { get; set; } + + /// Number of collectibles. + public long Collectibles { get; set; } + + /// Number of gear items. + public long Gear { get; set; } } diff --git a/src/WebApi.Contracts/Dto/GearDto.cs b/src/WebApi.Contracts/Dto/GearDto.cs new file mode 100644 index 00000000..56493a39 --- /dev/null +++ b/src/WebApi.Contracts/Dto/GearDto.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using Keeptrack.Common.System; + +namespace Keeptrack.WebApi.Contracts.Dto; + +/// +/// A piece of gear/equipment the user owns (e.g. a TV, a bike, a keyboard). +/// +public class GearDto : IHasId +{ + /// + /// Unique identifier. + /// + public string? Id { get; set; } + + /// + /// Gear name. + /// + /// Standing desk + public string? Title { get; set; } + + /// + /// Brand or manufacturer. + /// + /// Sony + public string? Brand { get; set; } + + /// + /// Free-text category (e.g. "Electronics", "Camping") - suggested from the tenant's other gear but + /// not a closed list. Unlike a blog's tags, one gear item has exactly one category. + /// + /// Electronics + public string? Category { get; set; } + + /// + /// Year. + /// + public int? Year { get; set; } + + /// + /// Free-text notes (where it's stored, where it's used, or any other specifics). + /// + public string? Notes { get; set; } + + /// + /// Tenant-owned cover image URL, shown on the list thumbnail and detail page header. Optional. + /// + public string? ImageUrl { get; set; } + + public bool IsFavorite { get; set; } + + /// + /// Every owned copy of this item - it counts as owned when this list is non-empty. + /// + public List OwnedVersions { get; set; } = []; + + /// + /// Filter-only query parameter: matches items with at least one owned version. Never populated on a + /// returned item - see for the convention. + /// + public bool IsOwned { get; set; } +} diff --git a/src/WebApi.Contracts/Dto/GenericVideoGameImportCommitItemDto.cs b/src/WebApi.Contracts/Dto/GenericVideoGameImportCommitItemDto.cs new file mode 100644 index 00000000..2190e454 --- /dev/null +++ b/src/WebApi.Contracts/Dto/GenericVideoGameImportCommitItemDto.cs @@ -0,0 +1,59 @@ +using System; + +namespace Keeptrack.WebApi.Contracts.Dto; + +/// +/// One row selected for import, carrying whatever the user edited in the review table. +/// +public class GenericVideoGameImportCommitItemDto +{ + /// + /// The this came from. Not used for anything + /// server-side beyond echoing it back in error messages - the row's data is taken entirely from this + /// DTO's own fields. + /// + public required string RowId { get; set; } + + public required string Title { get; set; } + + /// + /// The title exactly as the source export listed it, even if was edited in the + /// review UI - recorded in the created item's notes, since reference-data linking is expected to + /// overwrite later. + /// + public required string SourceTitle { get; set; } + + /// + /// Required and validated server-side - the export always carries one, but a user-edited value could be + /// blanked out by mistake. + /// + public string? Platform { get; set; } + + /// The store's own specific product/edition text - see . + public string? ProductName { get; set; } + + /// + /// The storefront's own transaction id and order id - together they're the server-derived owned-copy + /// Reference (see GenericVideoGameImportService.FormatReference in the Domain project), + /// which also doubles as the exact dedup key. Both are echoed back from the preview row rather than + /// accepting a client-supplied Reference string directly, so the format can't drift out of sync + /// with what a later re-preview checks against. + /// + public required string TransactionId { get; set; } + + public required string OrderId { get; set; } + + public required string Vendor { get; set; } + + /// + /// Publication/release year, if the user happens to know it - the export has no source for this, so it's + /// never auto-filled. + /// + public int? Year { get; set; } + + public DateOnly? AcquiredAt { get; set; } + + public decimal? Price { get; set; } + + public CopyType CopyType { get; set; } +} diff --git a/src/WebApi.Contracts/Dto/GenericVideoGameImportCommitRequestDto.cs b/src/WebApi.Contracts/Dto/GenericVideoGameImportCommitRequestDto.cs new file mode 100644 index 00000000..343ea569 --- /dev/null +++ b/src/WebApi.Contracts/Dto/GenericVideoGameImportCommitRequestDto.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Keeptrack.WebApi.Contracts.Dto; + +/// +/// The set of generic video game transaction rows the user picked in the review UI, ready to be created as +/// video games. +/// +public class GenericVideoGameImportCommitRequestDto +{ + public required List Items { get; set; } +} diff --git a/src/WebApi.Contracts/Dto/GenericVideoGameImportCommitResultDto.cs b/src/WebApi.Contracts/Dto/GenericVideoGameImportCommitResultDto.cs new file mode 100644 index 00000000..9a6e54a3 --- /dev/null +++ b/src/WebApi.Contracts/Dto/GenericVideoGameImportCommitResultDto.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace Keeptrack.WebApi.Contracts.Dto; + +/// +/// Outcome of committing a selected set of generic video game transaction rows. +/// +public class GenericVideoGameImportCommitResultDto +{ + /// Brand new video games created. + public int VideoGamesCreated { get; set; } + + /// Existing video games (including ones created earlier in this same commit) that received an additional platform entry. + public int VideoGamesMergedInto { get; set; } + + /// Rows whose transaction reference already matched an existing platform entry - not duplicated. + public int VideoGamesSkipped { get; set; } + + /// + /// The true per-row count of rows that got a platform entry added, whether the row's own new video game + /// or an existing/already-created-this-batch one. / + /// count distinct video games, not rows - several selected rows sharing a title can consolidate into + /// one created video game, which makes those two counts add up to less than the number of rows selected + /// even though nothing was lost. RowsImported + VideoGamesSkipped always equals the number of + /// rows submitted - the reconciling total to show the user so they can trust nothing was silently dropped. + /// + public int RowsImported { get; set; } + + /// The title of each row counted in , so the user can see exactly + /// which selected rows were treated as already-imported duplicates. + public List SkippedRowTitles { get; set; } = []; +} diff --git a/src/WebApi.Contracts/Dto/GenericVideoGameImportPreviewRowDto.cs b/src/WebApi.Contracts/Dto/GenericVideoGameImportPreviewRowDto.cs new file mode 100644 index 00000000..6010b16a --- /dev/null +++ b/src/WebApi.Contracts/Dto/GenericVideoGameImportPreviewRowDto.cs @@ -0,0 +1,48 @@ +using System; + +namespace Keeptrack.WebApi.Contracts.Dto; + +/// +/// One line item parsed from an uploaded generic video game transaction-history export, awaiting the user's +/// review before anything is imported. See for what gets +/// sent back once selected. +/// +public class GenericVideoGameImportPreviewRowDto +{ + /// + /// Correlates a selected/edited row back to this one at commit time. Stable within one export, never stored. + /// + public required string RowId { get; set; } + + /// + /// The game title, cleaned of a trailing platform suffix (e.g. "(PS4)") when present. + /// + public required string Title { get; set; } + + public required string Platform { get; set; } + + /// + /// The store's own specific product/edition text (e.g. "Grand Theft Auto V : Édition Premium"). + /// + public string? ProductName { get; set; } + + /// + /// The storefront the transaction was made on (e.g. "PlayStation Store"). + /// + public required string Vendor { get; set; } + + public required string TransactionId { get; set; } + + public required string OrderId { get; set; } + + public DateOnly? TransactionDate { get; set; } + + public decimal? Price { get; set; } + + /// + /// True when an existing video game already has a platform entry referencing this transaction. Defaults + /// to unchecked/hidden in the review UI, but stays editable/selectable in case the user wants to + /// re-import anyway. + /// + public bool AlreadyImported { get; set; } +} diff --git a/src/WebApi.Contracts/Dto/HealthProfileDto.cs b/src/WebApi.Contracts/Dto/HealthProfileDto.cs index 3a8e86b5..95e7a543 100644 --- a/src/WebApi.Contracts/Dto/HealthProfileDto.cs +++ b/src/WebApi.Contracts/Dto/HealthProfileDto.cs @@ -21,4 +21,9 @@ public class HealthProfileDto : IHasId /// Free-text notes (blood type, allergies, anything worth keeping at hand). /// public string? Notes { get; set; } + + /// + /// Tenant-owned cover image URL, shown on the list thumbnail and detail page header. Optional. + /// + public string? ImageUrl { get; set; } } diff --git a/src/WebApi.Contracts/Dto/HouseDto.cs b/src/WebApi.Contracts/Dto/HouseDto.cs index 683583f0..d2e4ffe4 100644 --- a/src/WebApi.Contracts/Dto/HouseDto.cs +++ b/src/WebApi.Contracts/Dto/HouseDto.cs @@ -42,4 +42,9 @@ public class HouseDto : IHasId /// Free-text notes. /// public string? Notes { get; set; } + + /// + /// Tenant-owned cover image URL, shown on the list thumbnail and detail page header. Optional. + /// + public string? ImageUrl { get; set; } } diff --git a/src/WebApi.Contracts/Dto/OwnedVersionDto.cs b/src/WebApi.Contracts/Dto/OwnedVersionDto.cs index 48b46fd3..11c0b1a4 100644 --- a/src/WebApi.Contracts/Dto/OwnedVersionDto.cs +++ b/src/WebApi.Contracts/Dto/OwnedVersionDto.cs @@ -32,4 +32,10 @@ public class OwnedVersionDto : IOwnedCopyDto /// Free-text reference for this copy: edition name, order number, barcode... /// public string? Reference { get; set; } + + /// + /// The store's own specific product/edition text for this copy (e.g. a retailer's listing title), + /// distinct from the item's own Title. Optional free text. + /// + public string? ProductName { get; set; } } diff --git a/src/WebApi.Contracts/Dto/UserPreferencesDto.cs b/src/WebApi.Contracts/Dto/UserPreferencesDto.cs new file mode 100644 index 00000000..d37d9372 --- /dev/null +++ b/src/WebApi.Contracts/Dto/UserPreferencesDto.cs @@ -0,0 +1,12 @@ +namespace Keeptrack.WebApi.Contracts.Dto; + +/// +/// The caller's own preferences. Always "mine" - never referenced by id, never listed. +/// +public class UserPreferencesDto +{ + /// + /// The caller's opt-in/opt-out feature toggles. + /// + public UserPreferencesFeaturesDto Features { get; set; } = new(); +} diff --git a/src/WebApi.Contracts/Dto/UserPreferencesFeaturesDto.cs b/src/WebApi.Contracts/Dto/UserPreferencesFeaturesDto.cs new file mode 100644 index 00000000..590adada --- /dev/null +++ b/src/WebApi.Contracts/Dto/UserPreferencesFeaturesDto.cs @@ -0,0 +1,15 @@ +namespace Keeptrack.WebApi.Contracts.Dto; + +public class UserPreferencesFeaturesDto +{ + /// + /// Shows an "open in a new tab" link to chasse-aux-livres.fr next to a book's ISBN field. + /// + public bool ShowChasseAuxLivresLink { get; set; } + + /// + /// Shows an "open on Amazon" link next to an owned copy's Reference field whenever it was created by + /// the Amazon order-history import (i.e. its Reference text carries an ASIN). + /// + public bool ShowAmazonProductLink { get; set; } +} diff --git a/src/WebApi.Contracts/Dto/VideoGameDto.cs b/src/WebApi.Contracts/Dto/VideoGameDto.cs index 10e1585c..f76141a9 100644 --- a/src/WebApi.Contracts/Dto/VideoGameDto.cs +++ b/src/WebApi.Contracts/Dto/VideoGameDto.cs @@ -35,11 +35,18 @@ public class VideoGameDto : IHasId, IReferenceLinkedDto public string? ReferenceId { get; set; } /// - /// Cover/poster image URL from the linked reference document - read-only, hydrated server-side on - /// list reads and never accepted from client input. + /// Cover image URL shown on the list page - when set, otherwise the linked + /// reference document's own cover. Read-only, hydrated server-side on list reads; never accepted from + /// client input (edit instead). /// public string? ImageUrl { get; set; } + /// + /// Tenant-owned cover image override, freely editable - takes priority over the linked reference's + /// cover wherever one is shown. Null means "use the reference's cover, if any". + /// + public string? CustomImageUrl { get; set; } + /// /// Filter-only query parameter: matches games with at least one platform entry (a game's copies). /// Never populated on a returned game - see for the convention. diff --git a/src/WebApi.Contracts/Dto/VideoGamePlatformDto.cs b/src/WebApi.Contracts/Dto/VideoGamePlatformDto.cs index 350fe40c..7a2b91fd 100644 --- a/src/WebApi.Contracts/Dto/VideoGamePlatformDto.cs +++ b/src/WebApi.Contracts/Dto/VideoGamePlatformDto.cs @@ -63,4 +63,10 @@ public class VideoGamePlatformDto : IOwnedCopyDto /// Free-text reference for this copy: edition name, order number, barcode... /// public string? Reference { get; set; } + + /// + /// The store's own specific product/edition text for this copy (e.g. "Grand Theft Auto V : Édition Premium"), + /// as opposed to the game's own title. + /// + public string? ProductName { get; set; } } diff --git a/src/WebApi/Controllers/AlbumController.cs b/src/WebApi/Controllers/AlbumController.cs index 7c4e8364..2c8722f2 100644 --- a/src/WebApi/Controllers/AlbumController.cs +++ b/src/WebApi/Controllers/AlbumController.cs @@ -21,10 +21,18 @@ public class AlbumController( { /// /// Hydrates each page item's cover image from its linked reference document - one batched lookup per - /// page (see ), keyed by the id-bearing documents only. + /// page (see ), keyed by the id-bearing documents only. An album with + /// its own set overrides that afterward - see + /// . /// - protected override Task OnListMappedAsync(List dtos) - => ReferenceImageHydrator.HydrateAsync(dtos, referenceRepository.FindByIdsAsync, x => x.ImageUrl); + protected override async Task OnListMappedAsync(List dtos) + { + await ReferenceImageHydrator.HydrateAsync(dtos, referenceRepository.FindByIdsAsync, x => x.ImageUrl); + foreach (var dto in dtos.Where(d => !string.IsNullOrEmpty(d.CustomImageUrl))) + { + dto.ImageUrl = dto.CustomImageUrl; + } + } /// /// Fires a best-effort background Discogs match for the new album - see . @@ -65,4 +73,21 @@ public async Task> RefreshReference(string id) model = await enrichmentService.TryLinkExistingAlbumReferenceAsync(model); return Ok(Mapper.ToDto(model)); } + + /// + /// Admin-only: clears this album's reference link and permanently deletes the shared reference + /// document - see . + /// + [HttpPost("{id}/unlink-reference")] + [Authorize(Policy = "AdminOnly")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public async Task> UnlinkReference(string id) + { + var model = await dataRepository.FindOneAsync(id, this.GetUserId()); + if (model is null) return NotFound(); + + model = await enrichmentService.UnlinkAlbumReferenceAsync(model); + return Ok(Mapper.ToDto(model)); + } } diff --git a/src/WebApi/Controllers/AmazonImportController.cs b/src/WebApi/Controllers/AmazonImportController.cs new file mode 100644 index 00000000..0c88ba58 --- /dev/null +++ b/src/WebApi/Controllers/AmazonImportController.cs @@ -0,0 +1,298 @@ +using System.Diagnostics.CodeAnalysis; +using Keeptrack.Common.System; +using Keeptrack.Domain.Models; +using Keeptrack.Domain.Repositories; +using Keeptrack.Domain.Services; +using Keeptrack.WebApi.Mappers; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Keeptrack.WebApi.Controllers; + +/// +/// Previews an Amazon.fr order-history export and commits the rows the user selected/edited in the review +/// UI as books, movies, TV shows, video games, gear, or collectibles (picked per row - see ). +/// Synchronous on both ends: unlike the TV Time import, there is no external API call in the loop, so even +/// a multi-year export completes well within a normal request. +/// +[ApiController] +[Authorize(Policy = "MemberOnly")] +[Route("api/import/amazon")] +public class AmazonImportController( + IBookRepository bookRepository, + IMovieRepository movieRepository, + ITvShowRepository tvShowRepository, + IVideoGameRepository videoGameRepository, + IGearRepository gearRepository, + ICollectibleRepository collectibleRepository, + AmazonOrderPreviewRowDtoMapper previewMapper) : ControllerBase +{ + /// + /// Parses the uploaded order-history CSV and returns every line item for review - nothing is persisted + /// by this call. Amazon's export carries no category column, so every row is returned; the review UI + /// defaults to showing only rows. + /// + [HttpPost("preview")] + [RequestSizeLimit(20_000_000)] + [Consumes("multipart/form-data")] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + [SuppressMessage("Security", "S5693:Make sure the content length limit is safe here", + Justification = "The limit IS set (20 MB), deliberately above Sonar's 8 MB default: a multi-year Amazon order-history " + + "export can be sizeable, and the endpoint is authenticated, member-only, admin-of-your-own-data.")] + public async Task>> Preview(IFormFile file) + { + if (file.Length == 0) + { + return BadRequest(); + } + + var ownerId = this.GetUserId(); + + // "Already imported" must be checked across every type, not just books - a row previously imported + // as a movie must still be flagged when the same export is uploaded again. + var existingBooks = await FindAllAsync(bookRepository, ownerId, new BookModel { OwnerId = ownerId, Title = string.Empty, Author = string.Empty }); + var existingMovies = await FindAllAsync(movieRepository, ownerId, new MovieModel { OwnerId = ownerId, Title = string.Empty }); + var existingTvShows = await FindAllAsync(tvShowRepository, ownerId, new TvShowModel { OwnerId = ownerId, Title = string.Empty }); + var existingVideoGames = await FindAllAsync(videoGameRepository, ownerId, new VideoGameModel { OwnerId = ownerId, Title = string.Empty }); + var existingGear = await FindAllAsync(gearRepository, ownerId, new GearModel { OwnerId = ownerId, Title = string.Empty }); + var existingCollectibles = await FindAllAsync(collectibleRepository, ownerId, new CollectibleModel { OwnerId = ownerId, Title = string.Empty }); + + var alreadyImportedReferences = new HashSet(); + alreadyImportedReferences.UnionWith(OwnedItemImportMergeService.FindImportedReferences(existingBooks, b => b.OwnedVersions.Select(v => v.Reference))); + alreadyImportedReferences.UnionWith(OwnedItemImportMergeService.FindImportedReferences(existingMovies, m => m.OwnedVersions.Select(v => v.Reference))); + alreadyImportedReferences.UnionWith(OwnedItemImportMergeService.FindImportedReferences(existingTvShows, t => t.OwnedVersions.Select(v => v.Reference))); + alreadyImportedReferences.UnionWith(OwnedItemImportMergeService.FindImportedReferences(existingVideoGames, g => g.Platforms.Select(p => p.Reference))); + alreadyImportedReferences.UnionWith(OwnedItemImportMergeService.FindImportedReferences(existingGear, g => g.OwnedVersions.Select(v => v.Reference))); + alreadyImportedReferences.UnionWith(OwnedItemImportMergeService.FindImportedReferences(existingCollectibles, c => c.OwnedVersions.Select(v => v.Reference))); + + await using var stream = file.OpenReadStream(); + var rows = AmazonOrderPreviewService.BuildPreview(stream, alreadyImportedReferences); + + return Ok(rows.Select(previewMapper.ToDto).ToList()); + } + + /// + /// Creates/updates items from the rows the user selected in the review UI, grouped by the media type + /// each row was assigned. A row whose (normalized) title matches an existing item of the same type - or + /// one created earlier in this same request - gets an additional owned copy instead of a duplicate + /// item; see . + /// + [HttpPost("commit")] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + public async Task> Commit(AmazonImportCommitRequestDto request) + { + var ownerId = this.GetUserId(); + var result = new AmazonImportCommitResultDto(); + + var itemMissingMediaType = request.Items.FirstOrDefault(item => item.MediaType is null); + if (itemMissingMediaType is not null) + { + throw new ArgumentException($"A media type is required to import '{itemMissingMediaType.Title}'."); + } + + var bookItems = request.Items.Where(i => i.MediaType == AmazonImportMediaType.Book).ToList(); + var movieItems = request.Items.Where(i => i.MediaType == AmazonImportMediaType.Movie).ToList(); + var tvShowItems = request.Items.Where(i => i.MediaType == AmazonImportMediaType.TvShow).ToList(); + var videoGameItems = request.Items.Where(i => i.MediaType == AmazonImportMediaType.VideoGame).ToList(); + var gearItems = request.Items.Where(i => i.MediaType == AmazonImportMediaType.Gear).ToList(); + var collectibleItems = request.Items.Where(i => i.MediaType == AmazonImportMediaType.Collectible).ToList(); + + var videoGameItemMissingPlatform = videoGameItems.FirstOrDefault(item => string.IsNullOrWhiteSpace(item.Platform)); + if (videoGameItemMissingPlatform is not null) + { + throw new ArgumentException($"A platform is required to import '{videoGameItemMissingPlatform.Title}' as a video game."); + } + + if (bookItems.Count > 0) + { + var existingBooks = await FindAllAsync(bookRepository, ownerId, new BookModel { OwnerId = ownerId, Title = string.Empty, Author = string.Empty }); + var (created, mergedInto, skipped) = await CommitAsync( + bookRepository, existingBooks, bookItems.Select(ToOwnedItemRequestItem).ToList(), + b => b.Title, b => b.OwnedVersions.Select(v => v.Reference), + i => i.Title, i => i.OwnedVersion.Reference, + item => new BookModel + { + OwnerId = ownerId, + Title = item.Title, + Author = string.Empty, + Year = item.Year, + Isbn = item.Isbn, + Notes = AmazonImportMergeService.BuildAmazonProvenanceNotes(item.AmazonTitle, item.Isbn), + OwnedVersions = [item.OwnedVersion] + }, + (book, item) => book.OwnedVersions.Add(item.OwnedVersion), ownerId); + (result.BooksCreated, result.BooksMergedInto, result.BooksSkipped) = (created, mergedInto, skipped); + } + + if (movieItems.Count > 0) + { + var existingMovies = await FindAllAsync(movieRepository, ownerId, new MovieModel { OwnerId = ownerId, Title = string.Empty }); + var (created, mergedInto, skipped) = await CommitAsync( + movieRepository, existingMovies, movieItems.Select(ToOwnedItemRequestItem).ToList(), + m => m.Title, m => m.OwnedVersions.Select(v => v.Reference), + i => i.Title, i => i.OwnedVersion.Reference, + item => new MovieModel + { + OwnerId = ownerId, + Title = item.Title, + Year = item.Year, + Notes = AmazonImportMergeService.BuildAmazonProvenanceNotes(item.AmazonTitle, null), + OwnedVersions = [item.OwnedVersion] + }, + (movie, item) => movie.OwnedVersions.Add(item.OwnedVersion), ownerId); + (result.MoviesCreated, result.MoviesMergedInto, result.MoviesSkipped) = (created, mergedInto, skipped); + } + + if (tvShowItems.Count > 0) + { + var existingTvShows = await FindAllAsync(tvShowRepository, ownerId, new TvShowModel { OwnerId = ownerId, Title = string.Empty }); + var (created, mergedInto, skipped) = await CommitAsync( + tvShowRepository, existingTvShows, tvShowItems.Select(ToOwnedItemRequestItem).ToList(), + t => t.Title, t => t.OwnedVersions.Select(v => v.Reference), + i => i.Title, i => i.OwnedVersion.Reference, + item => new TvShowModel + { + OwnerId = ownerId, + Title = item.Title, + Year = item.Year, + Notes = AmazonImportMergeService.BuildAmazonProvenanceNotes(item.AmazonTitle, null), + OwnedVersions = [item.OwnedVersion] + }, + (tvShow, item) => tvShow.OwnedVersions.Add(item.OwnedVersion), ownerId); + (result.TvShowsCreated, result.TvShowsMergedInto, result.TvShowsSkipped) = (created, mergedInto, skipped); + } + + if (videoGameItems.Count > 0) + { + var existingVideoGames = await FindAllAsync(videoGameRepository, ownerId, new VideoGameModel { OwnerId = ownerId, Title = string.Empty }); + var (created, mergedInto, skipped) = await CommitAsync( + videoGameRepository, existingVideoGames, videoGameItems.Select(ToVideoGameRequestItem).ToList(), + g => g.Title, g => g.Platforms.Select(p => p.Reference), + i => i.Title, i => i.Platform.Reference, + item => new VideoGameModel + { + OwnerId = ownerId, + Title = item.Title, + Year = item.Year, + Notes = AmazonImportMergeService.BuildAmazonProvenanceNotes(item.AmazonTitle, null), + Platforms = [item.Platform] + }, + (game, item) => game.Platforms.Add(item.Platform), ownerId); + (result.VideoGamesCreated, result.VideoGamesMergedInto, result.VideoGamesSkipped) = (created, mergedInto, skipped); + } + + if (gearItems.Count > 0) + { + var existingGear = await FindAllAsync(gearRepository, ownerId, new GearModel { OwnerId = ownerId, Title = string.Empty }); + var (created, mergedInto, skipped) = await CommitAsync( + gearRepository, existingGear, gearItems.Select(ToOwnedItemRequestItem).ToList(), + g => g.Title, g => g.OwnedVersions.Select(v => v.Reference), + i => i.Title, i => i.OwnedVersion.Reference, + item => new GearModel + { + OwnerId = ownerId, + Title = item.Title, + Year = item.Year, + Notes = AmazonImportMergeService.BuildAmazonProvenanceNotes(item.AmazonTitle, null), + OwnedVersions = [item.OwnedVersion] + }, + (gear, item) => gear.OwnedVersions.Add(item.OwnedVersion), ownerId); + (result.GearCreated, result.GearMergedInto, result.GearSkipped) = (created, mergedInto, skipped); + } + + if (collectibleItems.Count > 0) + { + var existingCollectibles = await FindAllAsync(collectibleRepository, ownerId, new CollectibleModel { OwnerId = ownerId, Title = string.Empty }); + var (created, mergedInto, skipped) = await CommitAsync( + collectibleRepository, existingCollectibles, collectibleItems.Select(ToOwnedItemRequestItem).ToList(), + c => c.Title, c => c.OwnedVersions.Select(v => v.Reference), + i => i.Title, i => i.OwnedVersion.Reference, + item => new CollectibleModel + { + OwnerId = ownerId, + Title = item.Title, + Year = item.Year, + Notes = AmazonImportMergeService.BuildAmazonProvenanceNotes(item.AmazonTitle, null), + OwnedVersions = [item.OwnedVersion] + }, + (collectible, item) => collectible.OwnedVersions.Add(item.OwnedVersion), ownerId); + (result.CollectiblesCreated, result.CollectiblesMergedInto, result.CollectiblesSkipped) = (created, mergedInto, skipped); + } + + return Ok(result); + } + + private static AmazonOwnedItemImportRequestItem ToOwnedItemRequestItem(AmazonImportCommitItemDto item) => new() + { + Title = item.Title, + AmazonTitle = item.AmazonTitle, + Year = item.Year, + Isbn = item.Isbn, + OwnedVersion = ToOwnedVersion(item) + }; + + private static AmazonVideoGameImportRequestItem ToVideoGameRequestItem(AmazonImportCommitItemDto item) => new() + { + Title = item.Title, + AmazonTitle = item.AmazonTitle, + Year = item.Year, + Platform = new VideoGamePlatformModel + { + Platform = item.Platform!, + CopyType = ToDomainCopyType(item.CopyType), + Price = item.Price, + Vendor = item.Vendor, + AcquiredAt = item.AcquiredAt, + Reference = AmazonImportMergeService.FormatOrderReference(item.OrderId, item.Asin) + } + }; + + private static OwnedVersionModel ToOwnedVersion(AmazonImportCommitItemDto item) => new() + { + CopyType = ToDomainCopyType(item.CopyType), + Price = item.Price, + Vendor = item.Vendor, + AcquiredAt = item.AcquiredAt, + // Derived server-side from the order id + ASIN the preview row reported, never from a client-supplied + // Reference string - this is what disambiguates two different items sharing one Amazon order. + Reference = AmazonImportMergeService.FormatOrderReference(item.OrderId, item.Asin) + }; + + private static Keeptrack.Domain.Models.CopyType ToDomainCopyType(Keeptrack.WebApi.Contracts.Dto.CopyType copyType) => + Enum.Parse(copyType.ToString()); + + private static async Task<(int Created, int MergedInto, int Skipped)> CommitAsync( + IDataRepository repository, + List existingItems, + List requestItems, + Func getExistingTitle, + Func> getExistingReferences, + Func getItemTitle, + Func getItemReference, + Func createNew, + Action appendOwnedCopy, + string ownerId) + where TModel : class, IHasIdAndOwnerId + { + var plan = OwnedItemImportMergeService.ComputeCommitPlan( + existingItems, requestItems, getExistingTitle, getExistingReferences, getItemTitle, getItemReference, createNew, appendOwnedCopy); + + foreach (var item in plan.ItemsToCreate) + { + await repository.CreateAsync(item); + } + + foreach (var item in plan.ItemsToUpdate) + { + await repository.UpdateAsync(item.Id!, item, ownerId); + } + + return (plan.ItemsToCreate.Count, plan.ItemsToUpdate.Count, plan.OwnedCopiesSkipped); + } + + private static async Task> FindAllAsync(IDataRepository repository, string ownerId, TModel blankSample) + where TModel : IHasIdAndOwnerId => + (await repository.FindAllAsync(ownerId, 1, int.MaxValue, null, blankSample)).Items; +} diff --git a/src/WebApi/Controllers/BookController.cs b/src/WebApi/Controllers/BookController.cs index ee55bd58..b978c64e 100644 --- a/src/WebApi/Controllers/BookController.cs +++ b/src/WebApi/Controllers/BookController.cs @@ -74,4 +74,21 @@ public async Task> RefreshReference(string id) model = await enrichmentService.TryLinkExistingBookReferenceAsync(model); return Ok(Mapper.ToDto(model)); } + + /// + /// Admin-only: clears this book's reference link and permanently deletes the shared reference + /// document - see . + /// + [HttpPost("{id}/unlink-reference")] + [Authorize(Policy = "AdminOnly")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public async Task> UnlinkReference(string id) + { + var model = await dataRepository.FindOneAsync(id, this.GetUserId()); + if (model is null) return NotFound(); + + model = await enrichmentService.UnlinkBookReferenceAsync(model); + return Ok(Mapper.ToDto(model)); + } } diff --git a/src/WebApi/Controllers/CollectibleController.cs b/src/WebApi/Controllers/CollectibleController.cs new file mode 100644 index 00000000..cba1b80a --- /dev/null +++ b/src/WebApi/Controllers/CollectibleController.cs @@ -0,0 +1,13 @@ +using Keeptrack.Domain.Models; +using Keeptrack.Domain.Repositories; +using Keeptrack.WebApi.Mappers; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Keeptrack.WebApi.Controllers; + +[ApiController] +[Authorize(Policy = "MemberOnly")] +[Route("api/collectibles")] +public class CollectibleController(IDtoMapper mapper, ICollectibleRepository dataRepository) + : DataCrudControllerBase(mapper, dataRepository); diff --git a/src/WebApi/Controllers/GearController.cs b/src/WebApi/Controllers/GearController.cs new file mode 100644 index 00000000..5a7b0d6f --- /dev/null +++ b/src/WebApi/Controllers/GearController.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Keeptrack.Domain.Models; +using Keeptrack.Domain.Repositories; +using Keeptrack.WebApi.Mappers; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Keeptrack.WebApi.Controllers; + +[ApiController] +[Authorize(Policy = "MemberOnly")] +[Route("api/gear")] +public class GearController(IDtoMapper mapper, IGearRepository dataRepository) + : DataCrudControllerBase(mapper, dataRepository) +{ + /// + /// Distinct categories already used across this tenant's gear - feeds the list page's category + /// filter buttons and the detail page's suggested values. See + /// . + /// + [HttpGet("categories")] + [ProducesResponseType(200)] + public async Task>> GetCategories() + { + var categories = await dataRepository.FindDistinctCategoriesAsync(this.GetUserId()); + return Ok(categories); + } +} diff --git a/src/WebApi/Controllers/GenericVideoGameImportController.cs b/src/WebApi/Controllers/GenericVideoGameImportController.cs new file mode 100644 index 00000000..c9be506f --- /dev/null +++ b/src/WebApi/Controllers/GenericVideoGameImportController.cs @@ -0,0 +1,137 @@ +using System.Diagnostics.CodeAnalysis; +using Keeptrack.Domain.Models; +using Keeptrack.Domain.Repositories; +using Keeptrack.Domain.Services; +using Keeptrack.WebApi.Mappers; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Keeptrack.WebApi.Controllers; + +/// +/// Previews a generic video game transaction-history export and commits the rows the user selected/edited in +/// the review UI as video games. Unlike , every row is always a video +/// game (the export is store purchase history for games specifically), so there's no per-row media-type +/// picker and no generic multi-type commit branching - just one direct call into the shared +/// engine. +/// +[ApiController] +[Authorize(Policy = "MemberOnly")] +[Route("api/import/video-games")] +public class GenericVideoGameImportController( + IVideoGameRepository videoGameRepository, + GenericVideoGameImportPreviewRowDtoMapper previewMapper) : ControllerBase +{ + /// + /// Parses the uploaded transaction-history CSV and returns every line item for review - nothing is + /// persisted by this call. + /// + [HttpPost("preview")] + [RequestSizeLimit(20_000_000)] + [Consumes("multipart/form-data")] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + [SuppressMessage("Security", "S5693:Make sure the content length limit is safe here", + Justification = "The limit IS set (20 MB), deliberately above Sonar's 8 MB default: a multi-year " + + "transaction-history export can be sizeable, and the endpoint is authenticated, member-only, admin-of-your-own-data.")] + public async Task>> Preview(IFormFile file) + { + if (file.Length == 0) + { + return BadRequest(); + } + + var ownerId = this.GetUserId(); + + var existingVideoGames = await FindAllAsync(ownerId); + var alreadyImportedReferences = OwnedItemImportMergeService.FindImportedReferences(existingVideoGames, g => g.Platforms.Select(p => p.Reference)); + + await using var stream = file.OpenReadStream(); + var rows = GenericVideoGameImportService.BuildPreview(stream, alreadyImportedReferences); + + return Ok(rows.Select(previewMapper.ToDto).ToList()); + } + + /// + /// Creates/updates video games from the rows the user selected in the review UI. A row whose (normalized) + /// title matches an existing video game - or one created earlier in this same request - gets an + /// additional platform entry instead of a duplicate item; see + /// . + /// + [HttpPost("commit")] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + public async Task> Commit(GenericVideoGameImportCommitRequestDto request) + { + var ownerId = this.GetUserId(); + + var itemMissingPlatform = request.Items.FirstOrDefault(item => string.IsNullOrWhiteSpace(item.Platform)); + if (itemMissingPlatform is not null) + { + throw new ArgumentException($"A platform is required to import '{itemMissingPlatform.Title}'."); + } + + var existingVideoGames = await FindAllAsync(ownerId); + var requestItems = request.Items.Select(ToRequestItem).ToList(); + + var plan = OwnedItemImportMergeService.ComputeCommitPlan( + existingVideoGames, requestItems, + g => g.Title, g => g.Platforms.Select(p => p.Reference), + i => i.Title, i => i.Platform.Reference, + item => new VideoGameModel + { + OwnerId = ownerId, + Title = item.Title, + Year = item.Year, + Notes = GenericVideoGameImportService.BuildProvenanceNotes(item.Platform.Vendor!, item.SourceTitle), + Platforms = [item.Platform] + }, + (game, item) => game.Platforms.Add(item.Platform)); + + foreach (var item in plan.ItemsToCreate) + { + await videoGameRepository.CreateAsync(item); + } + + foreach (var item in plan.ItemsToUpdate) + { + await videoGameRepository.UpdateAsync(item.Id!, item, ownerId); + } + + return Ok(new GenericVideoGameImportCommitResultDto + { + VideoGamesCreated = plan.ItemsToCreate.Count, + VideoGamesMergedInto = plan.ItemsToUpdate.Count, + VideoGamesSkipped = plan.OwnedCopiesSkipped, + RowsImported = plan.OwnedCopiesAdded, + SkippedRowTitles = plan.SkippedTitles + }); + } + + private static GenericVideoGameImportRequestItem ToRequestItem(GenericVideoGameImportCommitItemDto item) => new() + { + Title = item.Title, + SourceTitle = item.SourceTitle, + Year = item.Year, + Platform = new VideoGamePlatformModel + { + Platform = item.Platform!, + CopyType = ToDomainCopyType(item.CopyType), + ProductName = item.ProductName, + Price = item.Price, + Vendor = item.Vendor, + AcquiredAt = item.AcquiredAt, + // Derived server-side from the transaction id + order id + product name the preview row + // reported, never from a client-supplied Reference string - the product name is what + // disambiguates two different items sharing one transaction/order (a single transaction can + // bundle several products), and the whole string doubles as the exact-match dedup key on a re-import. + Reference = GenericVideoGameImportService.FormatReference(item.TransactionId, item.OrderId, item.ProductName, item.SourceTitle) + } + }; + + private static Keeptrack.Domain.Models.CopyType ToDomainCopyType(Keeptrack.WebApi.Contracts.Dto.CopyType copyType) => + Enum.Parse(copyType.ToString()); + + private async Task> FindAllAsync(string ownerId) => + (await videoGameRepository.FindAllAsync(ownerId, 1, int.MaxValue, null, new VideoGameModel { OwnerId = ownerId, Title = string.Empty })).Items; +} diff --git a/src/WebApi/Controllers/MovieController.cs b/src/WebApi/Controllers/MovieController.cs index 0de482d1..733ffe91 100644 --- a/src/WebApi/Controllers/MovieController.cs +++ b/src/WebApi/Controllers/MovieController.cs @@ -67,4 +67,21 @@ public async Task> RefreshReference(string id) model = await enrichmentService.TryLinkExistingMovieReferenceAsync(model); return Ok(Mapper.ToDto(model)); } + + /// + /// Admin-only: clears this movie's reference link and permanently deletes the shared reference + /// document - see . + /// + [HttpPost("{id}/unlink-reference")] + [Authorize(Policy = "AdminOnly")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public async Task> UnlinkReference(string id) + { + var model = await dataRepository.FindOneAsync(id, this.GetUserId()); + if (model is null) return NotFound(); + + model = await enrichmentService.UnlinkMovieReferenceAsync(model); + return Ok(Mapper.ToDto(model)); + } } diff --git a/src/WebApi/Controllers/StatsController.cs b/src/WebApi/Controllers/StatsController.cs index 544f96c3..6eda553a 100644 --- a/src/WebApi/Controllers/StatsController.cs +++ b/src/WebApi/Controllers/StatsController.cs @@ -22,7 +22,9 @@ public class StatsController( IPlaylistRepository playlistRepository, IVideoGameRepository videoGameRepository, ICarRepository carRepository, - IHouseRepository houseRepository) : ControllerBase + IHouseRepository houseRepository, + ICollectibleRepository collectibleRepository, + IGearRepository gearRepository) : ControllerBase { /// /// How many items the caller has in each collection. @@ -43,7 +45,9 @@ public async Task> Get() Playlists = await playlistRepository.CountAsync(ownerId), VideoGames = await videoGameRepository.CountAsync(ownerId), Cars = await carRepository.CountAsync(ownerId), - Houses = await houseRepository.CountAsync(ownerId) + Houses = await houseRepository.CountAsync(ownerId), + Collectibles = await collectibleRepository.CountAsync(ownerId), + Gear = await gearRepository.CountAsync(ownerId) }); } } diff --git a/src/WebApi/Controllers/SystemStatusController.cs b/src/WebApi/Controllers/SystemStatusController.cs index e497e86d..d0f55008 100644 --- a/src/WebApi/Controllers/SystemStatusController.cs +++ b/src/WebApi/Controllers/SystemStatusController.cs @@ -41,7 +41,7 @@ public async Task> Get() { InstanceName = Environment.MachineName, IsReferenceSyncEnabled = appConfiguration.IsReferenceSyncEnabled, - // the *default* provider used for automatic/background resolution - see BookReferenceClientRegistry; + // the *default* provider used for automatic/background resolution - see BookReferenceClientRegistry. // an admin can search/link with any registered provider regardless of this value (GET book-providers) BookProvider = string.IsNullOrEmpty(appConfiguration.BookReferenceProvider) ? "googlebooks" : appConfiguration.BookReferenceProvider, ReferenceSyncLease = lease is null diff --git a/src/WebApi/Controllers/TvShowController.cs b/src/WebApi/Controllers/TvShowController.cs index bf5de1cc..b7f50e3a 100644 --- a/src/WebApi/Controllers/TvShowController.cs +++ b/src/WebApi/Controllers/TvShowController.cs @@ -69,4 +69,26 @@ public async Task> RefreshReference(string id) model = await enrichmentService.TryLinkExistingTvShowReferenceAsync(model); return Ok(Mapper.ToDto(model)); } + + /// + /// Admin-only: clears this show's reference link and permanently deletes the shared reference document + /// itself, rather than merely detaching this one tenant's link - the admin has determined the match was + /// wrong, so the document behind it is bad data. Unlike (open to any + /// owner, harmless/idempotent), this mutates shared data other tenants could theoretically point at, so + /// it's gated to admins via a method-level policy on top of the controller's own plain [Authorize]. + /// Clearing ReferenceId is also what makes the Blazor detail page's InlineReferenceLinker + /// search card reappear, letting the admin immediately pick the correct match. + /// + [HttpPost("{id}/unlink-reference")] + [Authorize(Policy = "AdminOnly")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public async Task> UnlinkReference(string id) + { + var model = await dataRepository.FindOneAsync(id, this.GetUserId()); + if (model is null) return NotFound(); + + model = await enrichmentService.UnlinkTvShowReferenceAsync(model); + return Ok(Mapper.ToDto(model)); + } } diff --git a/src/WebApi/Controllers/UserPreferencesController.cs b/src/WebApi/Controllers/UserPreferencesController.cs new file mode 100644 index 00000000..8c3d66a9 --- /dev/null +++ b/src/WebApi/Controllers/UserPreferencesController.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using Keeptrack.Domain.Models; +using Keeptrack.Domain.Repositories; +using Keeptrack.WebApi.Contracts.Dto; +using Keeptrack.WebApi.Mappers; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Keeptrack.WebApi.Controllers; + +/// +/// The caller's own opt-in/opt-out feature toggles - a singleton-per-owner resource (no id in the route, +/// never listed), so a plain like rather than +/// . Available to every authenticated user (no +/// "MemberOnly" policy) - these are UI preferences, not a quota-relevant feature. +/// +[ApiController] +[Authorize] +[Route("api/user-preferences")] +public class UserPreferencesController(IUserPreferencesRepository repository, IDtoMapper mapper) : ControllerBase +{ + /// + /// The caller's preferences, defaulting to all-off when nothing has been saved yet. + /// + [HttpGet] + [ProducesResponseType(200)] + public async Task> Get() + { + var ownerId = this.GetUserId(); + var model = await repository.FindByOwnerIdAsync(ownerId) ?? new UserPreferencesModel { OwnerId = ownerId }; + return Ok(mapper.ToDto(model)); + } + + /// + /// Replaces the caller's preferences. + /// + [HttpPut] + [ProducesResponseType(204)] + public async Task Put(UserPreferencesDto dto) + { + var ownerId = this.GetUserId(); + var model = mapper.ToModel(dto); + model.OwnerId = ownerId; + await repository.UpsertAsync(model); + return NoContent(); + } +} diff --git a/src/WebApi/Controllers/VideoGameController.cs b/src/WebApi/Controllers/VideoGameController.cs index b7d6d958..e8e9485c 100644 --- a/src/WebApi/Controllers/VideoGameController.cs +++ b/src/WebApi/Controllers/VideoGameController.cs @@ -21,10 +21,18 @@ public class VideoGameController( { /// /// Hydrates each page item's cover image from its linked reference document - one batched lookup per - /// page (see ), keyed by the id-bearing documents only. + /// page (see ), keyed by the id-bearing documents only. A game with + /// its own set overrides that afterward - see + /// . /// - protected override Task OnListMappedAsync(List dtos) - => ReferenceImageHydrator.HydrateAsync(dtos, referenceRepository.FindByIdsAsync, x => x.ImageUrl); + protected override async Task OnListMappedAsync(List dtos) + { + await ReferenceImageHydrator.HydrateAsync(dtos, referenceRepository.FindByIdsAsync, x => x.ImageUrl); + foreach (var dto in dtos.Where(d => !string.IsNullOrEmpty(d.CustomImageUrl))) + { + dto.ImageUrl = dto.CustomImageUrl; + } + } /// /// Fires a best-effort background RAWG match for the new game - see . @@ -64,4 +72,21 @@ public async Task> RefreshReference(string id) model = await enrichmentService.TryLinkExistingVideoGameReferenceAsync(model); return Ok(Mapper.ToDto(model)); } + + /// + /// Admin-only: clears this game's reference link and permanently deletes the shared reference + /// document - see . + /// + [HttpPost("{id}/unlink-reference")] + [Authorize(Policy = "AdminOnly")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public async Task> UnlinkReference(string id) + { + var model = await dataRepository.FindOneAsync(id, this.GetUserId()); + if (model is null) return NotFound(); + + model = await enrichmentService.UnlinkVideoGameReferenceAsync(model); + return Ok(Mapper.ToDto(model)); + } } diff --git a/src/WebApi/DependencyInjection/InfrastructureServiceCollectionExtensions.cs b/src/WebApi/DependencyInjection/InfrastructureServiceCollectionExtensions.cs index 133c8efe..ed5773ec 100644 --- a/src/WebApi/DependencyInjection/InfrastructureServiceCollectionExtensions.cs +++ b/src/WebApi/DependencyInjection/InfrastructureServiceCollectionExtensions.cs @@ -41,9 +41,12 @@ internal static void AddMongoDbInfrastructure(this IServiceCollection services, services.AddSingleton, HouseHistoryStorageMapper>(); services.AddSingleton, HealthProfileStorageMapper>(); services.AddSingleton, HealthRecordStorageMapper>(); + services.AddSingleton, CollectibleStorageMapper>(); + services.AddSingleton, GearStorageMapper>(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -54,6 +57,7 @@ internal static void AddMongoDbInfrastructure(this IServiceCollection services, services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); @@ -64,6 +68,8 @@ internal static void AddMongoDbInfrastructure(this IServiceCollection services, services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); diff --git a/src/WebApi/DependencyInjection/ProviderResilienceExtensions.cs b/src/WebApi/DependencyInjection/ProviderResilienceExtensions.cs index 111af315..1d43d845 100644 --- a/src/WebApi/DependencyInjection/ProviderResilienceExtensions.cs +++ b/src/WebApi/DependencyInjection/ProviderResilienceExtensions.cs @@ -15,4 +15,22 @@ internal static class ProviderResilienceExtensions /// internal static void AddProviderResilienceHandler(this IHttpClientBuilder builder) => builder.AddStandardResilienceHandler(options => options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(10)); + + /// + /// Same pipeline as , with a larger retry budget - confirmed + /// against the real API that Google Books returns a transient 500/503 noticeably more often than + /// TMDB/RAWG/Discogs, and 3 retries (the shared default) isn't always enough to ride it out. + /// Applied to all three book providers (not just Google Books) for consistency, since Open Library/BnF + /// share the same registry/fallback shape and there's no reason to special-case just one of the three. + /// The retry delay is shortened (1s base instead of the default 2s) so the extra attempts still fit + /// comfortably under the widened total budget instead of mostly being eaten by exponential backoff. + /// + internal static void AddBookProviderResilienceHandler(this IHttpClientBuilder builder) => + builder.AddStandardResilienceHandler(options => + { + options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(10); + options.Retry.MaxRetryAttempts = 5; + options.Retry.Delay = TimeSpan.FromSeconds(1); + options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(40); + }); } diff --git a/src/WebApi/Import/CarHistoryImportService.cs b/src/WebApi/Import/CarHistoryImportService.cs index ff1ff380..ef1e5cb6 100644 --- a/src/WebApi/Import/CarHistoryImportService.cs +++ b/src/WebApi/Import/CarHistoryImportService.cs @@ -174,7 +174,7 @@ private static Dictionary ReadHeaders(IXLWorksheet sheet) var headers = new Dictionary(); foreach (var cell in headerRow.CellsUsed()) { - // Some headers (e.g. "Autoroute\nEloigné") span two lines within a single cell; + // Some headers (e.g. "Autoroute\nEloigné") span two lines within a single cell - // normalize whitespace so a lookup doesn't have to guess the exact line-break character used in the file. var text = string.Join(' ', cell.GetString().Split([' ', '\r', '\n', '\t'], StringSplitOptions.RemoveEmptyEntries)); if (text.Length > 0) headers.TryAdd(text, cell.Address.ColumnNumber); diff --git a/src/WebApi/Import/HealthImportService.cs b/src/WebApi/Import/HealthImportService.cs index de5aef38..9a733dfa 100644 --- a/src/WebApi/Import/HealthImportService.cs +++ b/src/WebApi/Import/HealthImportService.cs @@ -78,58 +78,74 @@ public async Task ImportAsync(Stream xlsxStream, string o foreach (var row in sheet.RowsUsed().Skip(1)) { - var dateCell = row.Cell(columns.Date.Value); - if (dateCell.IsEmpty()) continue; - if (!dateCell.TryGetValue(out DateTime date)) - { - result.Warnings.Add($"Row {row.RowNumber()}: skipped, unreadable date (\"{dateCell.GetString()}\")."); - continue; - } - - var personName = ExcelCellParser.StringOrNull(Cell(row, columns.Person)); - if (personName is null) - { - result.Warnings.Add($"Row {row.RowNumber()}: skipped, no \"Personne\" to attach the entry to."); - continue; - } + await ImportRowAsync(row, columns, ownerId, profilesByName, result); + } - if (!profilesByName.TryGetValue(personName, out var target)) - { - target = await healthProfileRepository.CreateAsync(new HealthProfileModel { OwnerId = ownerId, Name = personName }); - profilesByName[personName] = target; - result.ProfilesCreated++; - } + result.ProfilesSkipped = profilesByName.Count - result.ProfilesCreated; + return result; + } - var time = ExcelCellParser.ParseTime(Cell(row, columns.Time)) ?? TimeOnly.MinValue; + private async Task ImportRowAsync(IXLRow row, SheetColumns columns, string ownerId, + Dictionary profilesByName, HealthImportResultDto result) + { + var dateCell = row.Cell(columns.Date!.Value); + if (dateCell.IsEmpty()) return; + if (!dateCell.TryGetValue(out DateTime date)) + { + result.Warnings.Add($"Row {row.RowNumber()}: skipped, unreadable date (\"{dateCell.GetString()}\")."); + return; + } - await healthRecordRepository.CreateAsync(new HealthRecordModel - { - OwnerId = ownerId, - HealthProfileId = target.Id!, - HistoryDate = DateOnly.FromDateTime(date).ToDateTime(time), - // every row in this journal is a care event; sicknesses weren't tracked in the spreadsheet - EventType = HealthEventType.Appointment, - Specialty = ExcelCellParser.StringOrNull(Cell(row, columns.Specialty)), - Practitioner = ExcelCellParser.StringOrNull(Cell(row, columns.Practitioner)), - Description = ExcelCellParser.StringOrNull(Cell(row, columns.Description)), - Price = ExcelCellParser.PriceOrNull(Cell(row, columns.Paid)), - PublicReimbursement = ExcelCellParser.PriceOrNull(Cell(row, columns.PublicReimbursement)), - InsuranceReimbursement = ExcelCellParser.PriceOrNull(Cell(row, columns.InsuranceReimbursement)), - // the sparse bookkeeping columns (place, actual ameli bank transfer, payment dates, comment) have no dedicated fields - - // preserved as labeled notes instead of dropped - Notes = ExcelCellParser.JoinNonEmpty("; ", - ExcelCellParser.StringOrNull(Cell(row, columns.Notes)), - ExcelCellParser.StringOrNull(Cell(row, columns.Location)) is { } location ? $"Lieu : {location}" : null, - ExcelCellParser.PriceOrNull(Cell(row, columns.PublicTransfer)) is { } transfer ? $"Virement Ameli : {transfer:F2}" : null, - DateTextOrNull(Cell(row, columns.PublicDate)) is { } publicDate ? $"Payé Ameli le {publicDate}" : null, - DateTextOrNull(Cell(row, columns.InsuranceDate)) is { } insuranceDate ? $"Payé mutuelle le {insuranceDate}" : null, - ExcelCellParser.StringOrNull(Cell(row, columns.Comment))) - }); - result.RecordsCreated++; + var personName = ExcelCellParser.StringOrNull(Cell(row, columns.Person)); + if (personName is null) + { + result.Warnings.Add($"Row {row.RowNumber()}: skipped, no \"Personne\" to attach the entry to."); + return; } - result.ProfilesSkipped = profilesByName.Count - result.ProfilesCreated; - return result; + var target = await GetOrCreateProfileAsync(personName, ownerId, profilesByName, result); + await healthRecordRepository.CreateAsync(BuildRecord(row, columns, ownerId, target, date)); + result.RecordsCreated++; + } + + private async Task GetOrCreateProfileAsync(string personName, string ownerId, + Dictionary profilesByName, HealthImportResultDto result) + { + if (profilesByName.TryGetValue(personName, out var target)) return target; + + target = await healthProfileRepository.CreateAsync(new HealthProfileModel { OwnerId = ownerId, Name = personName }); + profilesByName[personName] = target; + result.ProfilesCreated++; + return target; + } + + private static HealthRecordModel BuildRecord(IXLRow row, SheetColumns columns, string ownerId, HealthProfileModel target, DateTime date) + { + var time = ExcelCellParser.ParseTime(Cell(row, columns.Time)) ?? TimeOnly.MinValue; + + return new HealthRecordModel + { + OwnerId = ownerId, + HealthProfileId = target.Id!, + HistoryDate = DateOnly.FromDateTime(date).ToDateTime(time), + // every row in this journal is a care event; sicknesses weren't tracked in the spreadsheet + EventType = HealthEventType.Appointment, + Specialty = ExcelCellParser.StringOrNull(Cell(row, columns.Specialty)), + Practitioner = ExcelCellParser.StringOrNull(Cell(row, columns.Practitioner)), + Description = ExcelCellParser.StringOrNull(Cell(row, columns.Description)), + Price = ExcelCellParser.PriceOrNull(Cell(row, columns.Paid)), + PublicReimbursement = ExcelCellParser.PriceOrNull(Cell(row, columns.PublicReimbursement)), + InsuranceReimbursement = ExcelCellParser.PriceOrNull(Cell(row, columns.InsuranceReimbursement)), + // the sparse bookkeeping columns (place, actual ameli bank transfer, payment dates, comment) have no dedicated fields - + // preserved as labeled notes instead of dropped + Notes = ExcelCellParser.JoinNonEmpty("; ", + ExcelCellParser.StringOrNull(Cell(row, columns.Notes)), + ExcelCellParser.StringOrNull(Cell(row, columns.Location)) is { } location ? $"Lieu : {location}" : null, + ExcelCellParser.PriceOrNull(Cell(row, columns.PublicTransfer)) is { } transfer ? $"Virement Ameli : {transfer:F2}" : null, + DateTextOrNull(Cell(row, columns.PublicDate)) is { } publicDate ? $"Payé Ameli le {publicDate}" : null, + DateTextOrNull(Cell(row, columns.InsuranceDate)) is { } insuranceDate ? $"Payé mutuelle le {insuranceDate}" : null, + ExcelCellParser.StringOrNull(Cell(row, columns.Comment))) + }; } private static SheetColumns ReadColumns(IXLWorksheet sheet) diff --git a/src/WebApi/Mappers/AmazonOrderPreviewRowDtoMapper.cs b/src/WebApi/Mappers/AmazonOrderPreviewRowDtoMapper.cs new file mode 100644 index 00000000..7e058b0e --- /dev/null +++ b/src/WebApi/Mappers/AmazonOrderPreviewRowDtoMapper.cs @@ -0,0 +1,15 @@ +using Keeptrack.Domain.Models; +using Riok.Mapperly.Abstractions; + +namespace Keeptrack.WebApi.Mappers; + +/// +/// One-directional (Model -> Dto): is pure and +/// knows nothing about the web contract, so maps its rows +/// here - same shape as . +/// +[Mapper] +public partial class AmazonOrderPreviewRowDtoMapper +{ + public partial AmazonOrderPreviewRowDto ToDto(AmazonOrderPreviewRow model); +} diff --git a/src/WebApi/Mappers/CollectibleDtoMapper.cs b/src/WebApi/Mappers/CollectibleDtoMapper.cs new file mode 100644 index 00000000..d9b38d6f --- /dev/null +++ b/src/WebApi/Mappers/CollectibleDtoMapper.cs @@ -0,0 +1,16 @@ +using Keeptrack.Domain.Models; +using Riok.Mapperly.Abstractions; + +namespace Keeptrack.WebApi.Mappers; + +[Mapper] +[UseStaticMapper(typeof(CommonDtoMappings))] +public partial class CollectibleDtoMapper : IDtoMapper +{ + // see BookDtoMapper.ToModel for why MapValue (not MapperIgnoreTarget) is required here + [MapValue(nameof(CollectibleModel.OwnerId), "")] + public partial CollectibleModel ToModel(CollectibleDto dto); + + [MapperIgnoreSource(nameof(CollectibleModel.OwnerId))] + public partial CollectibleDto ToDto(CollectibleModel model); +} diff --git a/src/WebApi/Mappers/GearDtoMapper.cs b/src/WebApi/Mappers/GearDtoMapper.cs new file mode 100644 index 00000000..1c8730c7 --- /dev/null +++ b/src/WebApi/Mappers/GearDtoMapper.cs @@ -0,0 +1,16 @@ +using Keeptrack.Domain.Models; +using Riok.Mapperly.Abstractions; + +namespace Keeptrack.WebApi.Mappers; + +[Mapper] +[UseStaticMapper(typeof(CommonDtoMappings))] +public partial class GearDtoMapper : IDtoMapper +{ + // see BookDtoMapper.ToModel for why MapValue (not MapperIgnoreTarget) is required here + [MapValue(nameof(GearModel.OwnerId), "")] + public partial GearModel ToModel(GearDto dto); + + [MapperIgnoreSource(nameof(GearModel.OwnerId))] + public partial GearDto ToDto(GearModel model); +} diff --git a/src/WebApi/Mappers/GenericVideoGameImportPreviewRowDtoMapper.cs b/src/WebApi/Mappers/GenericVideoGameImportPreviewRowDtoMapper.cs new file mode 100644 index 00000000..b51c8e12 --- /dev/null +++ b/src/WebApi/Mappers/GenericVideoGameImportPreviewRowDtoMapper.cs @@ -0,0 +1,15 @@ +using Keeptrack.Domain.Models; +using Riok.Mapperly.Abstractions; + +namespace Keeptrack.WebApi.Mappers; + +/// +/// One-directional (Model -> Dto): is pure and +/// knows nothing about the web contract, so maps +/// its rows here - same shape as . +/// +[Mapper] +public partial class GenericVideoGameImportPreviewRowDtoMapper +{ + public partial GenericVideoGameImportPreviewRowDto ToDto(GenericVideoGameImportPreviewRow model); +} diff --git a/src/WebApi/Mappers/UserPreferencesDtoMapper.cs b/src/WebApi/Mappers/UserPreferencesDtoMapper.cs new file mode 100644 index 00000000..3aa90ffa --- /dev/null +++ b/src/WebApi/Mappers/UserPreferencesDtoMapper.cs @@ -0,0 +1,22 @@ +using Keeptrack.Domain.Models; +using Riok.Mapperly.Abstractions; + +namespace Keeptrack.WebApi.Mappers; + +/// +/// Unlike every other , is only ever used to +/// build a fresh model for UpsertAsync, never to preserve an existing document's Id - the +/// repository upserts by OwnerId, not Id, so a null Id here is harmless (Mongo keeps +/// the existing document's own _id on a matched replace). +/// +[Mapper] +public partial class UserPreferencesDtoMapper : IDtoMapper +{ + [MapperIgnoreTarget(nameof(UserPreferencesModel.Id))] + [MapValue(nameof(UserPreferencesModel.OwnerId), "")] + public partial UserPreferencesModel ToModel(UserPreferencesDto dto); + + [MapperIgnoreSource(nameof(UserPreferencesModel.Id))] + [MapperIgnoreSource(nameof(UserPreferencesModel.OwnerId))] + public partial UserPreferencesDto ToDto(UserPreferencesModel model); +} diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index 6874aab8..d37048a6 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -19,6 +19,8 @@ builder.Services.AddSingleton, Keeptrack.WebApi.Mappers.HouseHistoryDtoMapper>(); builder.Services.AddSingleton, Keeptrack.WebApi.Mappers.HealthProfileDtoMapper>(); builder.Services.AddSingleton, Keeptrack.WebApi.Mappers.HealthRecordDtoMapper>(); +builder.Services.AddSingleton, Keeptrack.WebApi.Mappers.CollectibleDtoMapper>(); +builder.Services.AddSingleton, Keeptrack.WebApi.Mappers.GearDtoMapper>(); builder.Services.AddSingleton, Keeptrack.WebApi.Mappers.EpisodeDtoMapper>(); builder.Services.AddSingleton, Keeptrack.WebApi.Mappers.MovieDtoMapper>(); builder.Services.AddSingleton, Keeptrack.WebApi.Mappers.AlbumDtoMapper>(); @@ -26,7 +28,10 @@ builder.Services.AddSingleton, Keeptrack.WebApi.Mappers.SongDtoMapper>(); builder.Services.AddSingleton, Keeptrack.WebApi.Mappers.PlaylistDtoMapper>(); builder.Services.AddSingleton, Keeptrack.WebApi.Mappers.VideoGameDtoMapper>(); +builder.Services.AddSingleton, Keeptrack.WebApi.Mappers.UserPreferencesDtoMapper>(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -66,20 +71,20 @@ { client.BaseAddress = new Uri("https://www.googleapis.com/books/v1/"); client.Timeout = Timeout.InfiniteTimeSpan; -}).AddProviderResilienceHandler(); +}).AddBookProviderResilienceHandler(); builder.Services.AddTransient(sp => sp.GetRequiredService()); builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri("https://openlibrary.org/"); client.DefaultRequestHeaders.Add("User-Agent", "Keeptrack/1.0 (+https://github.com/devpro/keeptrack)"); client.Timeout = Timeout.InfiniteTimeSpan; -}).AddProviderResilienceHandler(); +}).AddBookProviderResilienceHandler(); builder.Services.AddTransient(sp => sp.GetRequiredService()); builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri("https://catalogue.bnf.fr/api/"); client.Timeout = Timeout.InfiniteTimeSpan; -}).AddProviderResilienceHandler(); +}).AddBookProviderResilienceHandler(); builder.Services.AddTransient(sp => sp.GetRequiredService()); // a factory (not a plain AddScoped) so configuration.BookReferenceProvider - // a plain computed-on-access property, not cached - is read fresh on every scope, same "checked fresh" @@ -123,7 +128,7 @@ builder.Services.AddAuthorization(options => { options.AddPolicy("AdminOnly", policy => policy.RequireClaim("role", "admin")); - // members (and admins - a membership must never be *less* than the owner's own account) get the full app; + // members (and admins - a membership must never be *less* than the owner's own account) get the full app. // authenticated users without the role are the free preview tier (movies + TV shows, capped - see DataCrudControllerBase). // Granted the same way as admin: a Firebase custom claim role=member via the Admin SDK (see CONTRIBUTING.md). options.AddPolicy("MemberOnly", policy => policy.RequireClaim("role", "member", "admin")); diff --git a/src/WebApi/ReferenceData/BnfClient.cs b/src/WebApi/ReferenceData/BnfClient.cs index b349e794..eea22499 100644 --- a/src/WebApi/ReferenceData/BnfClient.cs +++ b/src/WebApi/ReferenceData/BnfClient.cs @@ -26,6 +26,8 @@ public class BnfClient(HttpClient http) : IBookReferenceClient private const int MaxGenres = 5; + private static readonly TimeSpan s_regexTimeout = TimeSpan.FromSeconds(1); + /// /// is deliberately never sent to BnF as a query criterion, same choice /// makes (see its own doc comment) though for a different reason here: @@ -155,7 +157,7 @@ private static IEnumerable ParseRecords(string xml) /// URL, an ISBN as plain text "ISBN 2841142787" - confirmed against the real API) - the ARK is already /// captured separately via srw:recordIdentifier, so this only looks for the ISBN-prefixed one. /// - private static readonly Regex s_isbnRegex = new(@"^ISBN\s+(.+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex s_isbnRegex = new(@"^ISBN\s+(.+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled, s_regexTimeout); private static string? ExtractIsbn(IEnumerable identifiers) => identifiers @@ -175,7 +177,7 @@ private static IEnumerable ParseRecords(string xml) if (string.IsNullOrWhiteSpace(raw)) return null; var namePart = raw.Split(". ", 2)[0]; - namePart = Regex.Replace(namePart, @"\s*\([^)]*\)", "").Trim(); + namePart = Regex.Replace(namePart, @"\s*\([^)]*\)", "", RegexOptions.None, s_regexTimeout).Trim(); var parts = namePart.Split(", ", 2); return parts.Length == 2 ? $"{parts[1]} {parts[0]}".Trim() : namePart; @@ -186,7 +188,7 @@ private static IEnumerable ParseRecords(string xml) /// catalogue dates can carry uncertainty markers or ranges - a defensive last-4-digit-token extraction, /// same shape as 's own year parsing, rather than a bare int.Parse. /// - private static readonly Regex s_yearRegex = new(@"\b\d{4}\b", RegexOptions.Compiled); + private static readonly Regex s_yearRegex = new(@"\b\d{4}\b", RegexOptions.Compiled, s_regexTimeout); private static int? ParseYear(string? date) { diff --git a/src/WebApi/ReferenceData/GoogleBooksClient.cs b/src/WebApi/ReferenceData/GoogleBooksClient.cs index 473fc22a..7c1b388d 100644 --- a/src/WebApi/ReferenceData/GoogleBooksClient.cs +++ b/src/WebApi/ReferenceData/GoogleBooksClient.cs @@ -40,10 +40,25 @@ public async Task> SearchBooksAsync(string title return results; } + /// + /// Google's own fields= partial-response parameter (https://developers.google.com/books/docs/v1/performance), + /// restricted to exactly what reads - confirmed against the real API that + /// the unfiltered response is ~75KB/20 results (a full volumeInfo, including the long HTML + /// description and every industry identifier, per item) against TMDB's ~14KB for a comparable movie + /// search; this is why a Google Books lookup was observed to be consistently slower than the other three + /// providers even on a successful call, not just during today's provider-side 500/503s. + /// + private const string SearchFields = "fields=items(id,volumeInfo(title,authors,publishedDate,imageLinks/thumbnail))"; + + /// + /// Same idea as , restricted to what reads. + /// + private const string DetailsFields = "fields=volumeInfo(title,publishedDate,description,authors,categories,imageLinks/thumbnail,language,industryIdentifiers)"; + private async Task> SearchBooksCoreAsync(string query, CancellationToken cancellationToken) { var response = await http.GetFromJsonAsync( - $"volumes?q={Encode(query)}&maxResults={MaxResults}&key={ApiKey}", cancellationToken); + $"volumes?q={Encode(query)}&maxResults={MaxResults}&key={ApiKey}&{SearchFields}", cancellationToken); return response?.Items .Where(i => !string.IsNullOrEmpty(i.Id) && !string.IsNullOrEmpty(i.VolumeInfo?.Title)) @@ -54,7 +69,7 @@ private async Task> SearchBooksCoreAsync(string public async Task GetBookDetailsAsync(string externalId, CancellationToken cancellationToken = default) { - var volume = await http.GetFromJsonAsync($"volumes/{externalId}?key={ApiKey}", cancellationToken); + var volume = await http.GetFromJsonAsync($"volumes/{externalId}?key={ApiKey}&{DetailsFields}", cancellationToken); var info = volume?.VolumeInfo; if (info?.Title is null) return null; diff --git a/src/WebApi/ReferenceData/OpenLibraryClient.cs b/src/WebApi/ReferenceData/OpenLibraryClient.cs index f7081576..20b5467d 100644 --- a/src/WebApi/ReferenceData/OpenLibraryClient.cs +++ b/src/WebApi/ReferenceData/OpenLibraryClient.cs @@ -17,6 +17,8 @@ public class OpenLibraryClient(HttpClient http) : IBookReferenceClient public string DisplayName => "Open Library"; + private static readonly TimeSpan s_regexTimeout = TimeSpan.FromSeconds(1); + /// /// Deliberately does NOT filter server-side by : /// Open Library's first_publish_year is the work's ORIGINAL publication year, @@ -68,7 +70,7 @@ private async Task> SearchBooksCoreAsync(string string? authorExternalId = null; if (!string.IsNullOrEmpty(authorKey)) { - authorExternalId = authorKey.Split('/').Last(); + authorExternalId = authorKey.Split('/')[^1]; var author = await http.GetFromJsonAsync($"{authorKey}.json", cancellationToken); authorName = author?.Name; } @@ -124,7 +126,7 @@ JsonValueKind.Object when description.Value.TryGetProperty("value", out var valu /// Fellowship of the Ring" (OL27513W). The year is the last such token, since it's always what trails /// the day/month when both are present, and the only token when the date is a bare year. /// - private static readonly Regex s_yearRegex = new(@"\b\d{4}\b", RegexOptions.Compiled); + private static readonly Regex s_yearRegex = new(@"\b\d{4}\b", RegexOptions.Compiled, s_regexTimeout); private static int? ParseYear(string? date) { diff --git a/src/WebApi/ReferenceData/ReferenceEnrichmentService.Albums.cs b/src/WebApi/ReferenceData/ReferenceEnrichmentService.Albums.cs index 5d971e3f..418c6f0e 100644 --- a/src/WebApi/ReferenceData/ReferenceEnrichmentService.Albums.cs +++ b/src/WebApi/ReferenceData/ReferenceEnrichmentService.Albums.cs @@ -5,6 +5,8 @@ namespace Keeptrack.WebApi.ReferenceData; public partial class ReferenceEnrichmentService { + private const string DiscogsProviderKey = "discogs"; + /// /// User-triggered "check for reference match" for albums - see /// for the full rationale (this is the same local-only, @@ -54,6 +56,24 @@ public async Task TryLinkExistingAlbumReferenceAsync(AlbumModel mode return model; } + /// + /// Admin-triggered "unlink" for albums - see for the full + /// rationale (clears the tenant's link and permanently deletes the shared reference document, rather + /// than only detaching this one item). + /// + public async Task UnlinkAlbumReferenceAsync(AlbumModel model) + { + var referenceId = model.ReferenceId; + model.ReferenceId = string.Empty; + await albumRepository.UpdateAsync(model.Id!, model, model.OwnerId); + if (!string.IsNullOrEmpty(referenceId)) + { + await albumReferenceRepository.DeleteAsync(referenceId); + } + + return model; + } + /// /// Best-effort automatic match for albums - see . Passing /// narrows the Discogs search considerably - without it, a common album @@ -82,17 +102,17 @@ public async Task ResolveAlbumAsync(string title, int? year // see ResolveTvShowAsync's own comment - the title-only fallback (which reuses existing.Id for the // upsert) must not run when year is known but simply unconfirmed yet, or it risks overwriting an // unrelated same-titled reference document instead of just linking wrong - var existing = await albumReferenceRepository.FindByExternalIdAsync("discogs", externalId) + var existing = await albumReferenceRepository.FindByExternalIdAsync(DiscogsProviderKey, externalId) ?? (details.Artist is not null ? await albumReferenceRepository.FindByTitleYearAsync(title, year, details.Artist) : null); if (existing is null && year is null && details.Artist is not null) { existing = await albumReferenceRepository.FindByTitleAsync(title, details.Artist); } var externalIds = existing?.ExternalIds ?? new Dictionary(); - externalIds["discogs"] = externalId; + externalIds[DiscogsProviderKey] = externalId; var artistReferenceId = !string.IsNullOrEmpty(details.ArtistExternalId) - ? await ResolvePersonReferenceIdAsync("discogs", details.ArtistExternalId, details.Artist ?? "Unknown", null) + ? await ResolvePersonReferenceIdAsync(DiscogsProviderKey, details.ArtistExternalId, details.Artist ?? "Unknown", null) : existing?.ArtistReferenceId; var model = new AlbumReferenceModel @@ -124,7 +144,7 @@ public async Task ResolveAlbumAsync(string title, int? year /// public async Task<(AlbumReferenceModel Model, bool DataChanged)> RefreshAlbumReferenceAsync(AlbumReferenceModel reference, CancellationToken cancellationToken = default) { - var externalId = reference.ExternalIds.GetValueOrDefault("discogs"); + var externalId = reference.ExternalIds.GetValueOrDefault(DiscogsProviderKey); if (string.IsNullOrEmpty(externalId)) return (reference, false); var details = await discogsClient.GetAlbumDetailsAsync(externalId, cancellationToken); @@ -135,7 +155,7 @@ public async Task ResolveAlbumAsync(string title, int? year reference.Synopsis = details.Synopsis; if (!string.IsNullOrEmpty(details.ArtistExternalId)) { - reference.ArtistReferenceId = await ResolvePersonReferenceIdAsync("discogs", details.ArtistExternalId, details.Artist ?? "Unknown", null); + reference.ArtistReferenceId = await ResolvePersonReferenceIdAsync(DiscogsProviderKey, details.ArtistExternalId, details.Artist ?? "Unknown", null); } reference.Genres = details.Genres; reference.Tracks = MapTracks(details.Tracks); diff --git a/src/WebApi/ReferenceData/ReferenceEnrichmentService.Books.cs b/src/WebApi/ReferenceData/ReferenceEnrichmentService.Books.cs index a8203b06..6d402d15 100644 --- a/src/WebApi/ReferenceData/ReferenceEnrichmentService.Books.cs +++ b/src/WebApi/ReferenceData/ReferenceEnrichmentService.Books.cs @@ -57,6 +57,24 @@ public async Task TryLinkExistingBookReferenceAsync(BookModel model) return model; } + /// + /// Admin-triggered "unlink" for books - see for the full + /// rationale (clears the tenant's link and permanently deletes the shared reference document, rather + /// than only detaching this one item). + /// + public async Task UnlinkBookReferenceAsync(BookModel model) + { + var referenceId = model.ReferenceId; + model.ReferenceId = string.Empty; + await bookRepository.UpdateAsync(model.Id!, model, model.OwnerId); + if (!string.IsNullOrEmpty(referenceId)) + { + await bookReferenceRepository.DeleteAsync(referenceId); + } + + return model; + } + /// /// Best-effort automatic match for books - see . Always searches /// the deployment's *default* provider ( with a null diff --git a/src/WebApi/ReferenceData/ReferenceEnrichmentService.TvShowsAndMovies.cs b/src/WebApi/ReferenceData/ReferenceEnrichmentService.TvShowsAndMovies.cs index 967794ad..0150b4be 100644 --- a/src/WebApi/ReferenceData/ReferenceEnrichmentService.TvShowsAndMovies.cs +++ b/src/WebApi/ReferenceData/ReferenceEnrichmentService.TvShowsAndMovies.cs @@ -110,6 +110,43 @@ public async Task TryLinkExistingMovieReferenceAsync(MovieModel mode return model; } + /// + /// Admin-triggered "unlink" - clears this tenant's own and, unlike + /// the implicit clear-on-no-match branch inside , + /// permanently deletes the shared reference document itself (the whole point: the admin has determined + /// this specific match was wrong, so the document behind it is bad data, not just wrong for this tenant). + /// Deliberately doesn't check whether any other tenant document still points at the same reference id - + /// an accepted, rare edge case, not worth the complexity of guarding against. + /// + public async Task UnlinkTvShowReferenceAsync(TvShowModel model) + { + var referenceId = model.ReferenceId; + model.ReferenceId = string.Empty; + await tvShowRepository.UpdateAsync(model.Id!, model, model.OwnerId); + if (!string.IsNullOrEmpty(referenceId)) + { + await tvShowReferenceRepository.DeleteAsync(referenceId); + } + + return model; + } + + /// + /// Movie equivalent of . + /// + public async Task UnlinkMovieReferenceAsync(MovieModel model) + { + var referenceId = model.ReferenceId; + model.ReferenceId = string.Empty; + await movieRepository.UpdateAsync(model.Id!, model, model.OwnerId); + if (!string.IsNullOrEmpty(referenceId)) + { + await movieReferenceRepository.DeleteAsync(referenceId); + } + + return model; + } + /// /// Best-effort automatic match: does nothing if the search returns zero or more than one candidate, /// leaving the show unresolved for the admin queue instead of guessing. diff --git a/src/WebApi/ReferenceData/ReferenceEnrichmentService.VideoGames.cs b/src/WebApi/ReferenceData/ReferenceEnrichmentService.VideoGames.cs index 812e4b56..eea65aff 100644 --- a/src/WebApi/ReferenceData/ReferenceEnrichmentService.VideoGames.cs +++ b/src/WebApi/ReferenceData/ReferenceEnrichmentService.VideoGames.cs @@ -49,6 +49,24 @@ public async Task TryLinkExistingVideoGameReferenceAsync(VideoGa return model; } + /// + /// Admin-triggered "unlink" for video games - see for the full + /// rationale (clears the tenant's link and permanently deletes the shared reference document, rather + /// than only detaching this one item). + /// + public async Task UnlinkVideoGameReferenceAsync(VideoGameModel model) + { + var referenceId = model.ReferenceId; + model.ReferenceId = string.Empty; + await videoGameRepository.UpdateAsync(model.Id!, model, model.OwnerId); + if (!string.IsNullOrEmpty(referenceId)) + { + await videoGameReferenceRepository.DeleteAsync(referenceId); + } + + return model; + } + /// /// Best-effort automatic match for video games - see . /// diff --git a/test/BlazorApp.PlaywrightTests/Pages/BookDetailPage.cs b/test/BlazorApp.PlaywrightTests/Pages/BookDetailPage.cs index 17a24c1c..418f8e06 100644 --- a/test/BlazorApp.PlaywrightTests/Pages/BookDetailPage.cs +++ b/test/BlazorApp.PlaywrightTests/Pages/BookDetailPage.cs @@ -16,10 +16,4 @@ public class BookDetailPage(IPage page) : ReferenceableDetailPageBase(page) /// public async Task SelectProviderAsync(string displayName) => await Page.GetByRole(AriaRole.Button, new PageGetByRoleOptions { Name = displayName, Exact = true }).ClickAsync(); - - public static async Task SetFieldAsync(ILocator input, string value) - { - await input.FillAsync(value); - await input.BlurAsync(); - } } diff --git a/test/BlazorApp.PlaywrightTests/Pages/CollectibleDetailPage.cs b/test/BlazorApp.PlaywrightTests/Pages/CollectibleDetailPage.cs new file mode 100644 index 00000000..a3a4b767 --- /dev/null +++ b/test/BlazorApp.PlaywrightTests/Pages/CollectibleDetailPage.cs @@ -0,0 +1,8 @@ +using Microsoft.Playwright; + +namespace Keeptrack.BlazorApp.PlaywrightTests.Pages; + +/// +/// Collectibles have no reference-data concept, same as - a plain . +/// +public class CollectibleDetailPage(IPage page) : DetailPageBase(page); diff --git a/test/BlazorApp.PlaywrightTests/Pages/DetailPageBase.cs b/test/BlazorApp.PlaywrightTests/Pages/DetailPageBase.cs index c7a7f60f..6ba6698a 100644 --- a/test/BlazorApp.PlaywrightTests/Pages/DetailPageBase.cs +++ b/test/BlazorApp.PlaywrightTests/Pages/DetailPageBase.cs @@ -1,22 +1,53 @@ +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Playwright; namespace Keeptrack.BlazorApp.PlaywrightTests.Pages; /// -/// Shared shape for every item's own detail page (Book/Movie/TvShow/VideoGame/Album/Playlist/Car/House) - -/// every one of them renders its title/name in an <input class="kt-title-input">, a class -/// unique enough on the page to locate without any data-testid (unlike the list pages' Add-form -/// fields, which needed one since multiple plain .form-control inputs share no distinguishing class). +/// Shared shape for every item's own detail page (Book/Movie/TvShow/VideoGame/Album/Playlist/Car/House/ +/// HealthProfile/Collectible/Gear) - every one of them renders its title/name in an +/// <input class="kt-title-input">, a class unique enough on the page to locate without any +/// data-testid (unlike the list pages' Add-form fields, which needed one since multiple plain +/// .form-control inputs share no distinguishing class). /// -public abstract class DetailPageBase(IPage page) : PageBase(page) +public abstract partial class DetailPageBase(IPage page) : PageBase(page) { + [GeneratedRegex("(cover|poster)$")] + private static partial Regex CoverRegex(); + public ILocator TitleInput => Page.Locator(".kt-title-input"); + /// + /// The cover-image banner, when the page has one - every detail page's own image element ends its + /// alt text in "cover" (reference-hydrated types) or "poster" by the same convention. Not every + /// subclass renders one (Playlist has no cover concept), so this simply never resolves on those. + /// + public ILocator CoverImage => Page.GetByRole(AriaRole.Img, new PageGetByRoleOptions { NameRegex = CoverRegex() }); + + /// + /// The plain "Cover image URL" field on the five non-reference-linked types (Car/House/HealthProfile/ + /// Collectible/Gear) - data-testid="image-url-input", since the label/input pair has no + /// for/id association. Book/Movie/TvShow/VideoGame/Album's reference-hydrated equivalent + /// (CustomImageUrl) is a separate, not-yet-testid'd field, out of scope here. + /// + public ILocator ImageUrlInput => Page.GetByTestId("image-url-input"); + public override async Task WaitForReadyAsync() { await base.WaitForReadyAsync(); await Assertions.Expect(Page.Locator(".kt-spinner")).ToBeHiddenAsync(); await Assertions.Expect(TitleInput).ToBeVisibleAsync(); } + + /// + /// Fills a detail-page field bound via @onchange (not @bind) and blurs it - Blazor's + /// @onchange listens for the native "change" event, which only fires on blur once the value has + /// actually changed, not on Playwright's FillAsync alone (that only dispatches "input"). + /// + public static async Task SetFieldAsync(ILocator input, string value) + { + await input.FillAsync(value); + await input.BlurAsync(); + } } diff --git a/test/BlazorApp.PlaywrightTests/Pages/GearDetailPage.cs b/test/BlazorApp.PlaywrightTests/Pages/GearDetailPage.cs new file mode 100644 index 00000000..ee82b8b8 --- /dev/null +++ b/test/BlazorApp.PlaywrightTests/Pages/GearDetailPage.cs @@ -0,0 +1,8 @@ +using Microsoft.Playwright; + +namespace Keeptrack.BlazorApp.PlaywrightTests.Pages; + +/// +/// Gear has no reference-data concept, same as - a plain . +/// +public class GearDetailPage(IPage page) : DetailPageBase(page); diff --git a/test/BlazorApp.PlaywrightTests/Pages/PageBase.cs b/test/BlazorApp.PlaywrightTests/Pages/PageBase.cs index bf53a8f4..9526aa7f 100644 --- a/test/BlazorApp.PlaywrightTests/Pages/PageBase.cs +++ b/test/BlazorApp.PlaywrightTests/Pages/PageBase.cs @@ -93,5 +93,9 @@ private async Task NavigateAsync(string linkName, TPage next) wher public Task OpenHealthAsync() => NavigateAsync("Health", new ListPage(Page, "/health", "Health")); + public Task OpenCollectiblesAsync() => NavigateAsync("Collectibles", new ListPage(Page, "/collectibles", "Collectibles")); + + public Task OpenGearAsync() => NavigateAsync("Gear", new ListPage(Page, "/gear", "Gear")); + public Task LogoutAsync() => NavigateAsync("Log out", new HomePage(Page)); } diff --git a/test/BlazorApp.PlaywrightTests/Pages/ReferenceableDetailPageBase.cs b/test/BlazorApp.PlaywrightTests/Pages/ReferenceableDetailPageBase.cs index c5a5396d..975fae56 100644 --- a/test/BlazorApp.PlaywrightTests/Pages/ReferenceableDetailPageBase.cs +++ b/test/BlazorApp.PlaywrightTests/Pages/ReferenceableDetailPageBase.cs @@ -1,4 +1,3 @@ -using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Playwright; @@ -8,18 +7,14 @@ namespace Keeptrack.BlazorApp.PlaywrightTests.Pages; /// Shared shape for the five detail pages that carry a reference-data concept (Book/Movie/TvShow/VideoGame/Album) - /// all five render the exact same "check for reference match" icon button + toast, /// and the exact same admin-only InlineReferenceLinker (search the real provider, pick a candidate, click "Link"). +/// is inherited from - shared with the non-reference-linked types too. /// -public abstract partial class ReferenceableDetailPageBase(IPage page) : DetailPageBase(page) +public abstract class ReferenceableDetailPageBase(IPage page) : DetailPageBase(page) { - [GeneratedRegex("(cover|poster)$")] - private static partial Regex CoverRegex(); - private ILocator RefreshReferenceButton => Page.Locator("button.kt-icon-btn"); private ILocator ReferenceToast => Page.Locator(".kt-inline-toast"); - public ILocator CoverImage => Page.GetByRole(AriaRole.Img, new PageGetByRoleOptions { NameRegex = CoverRegex() }); - /// /// The non-admin, local-only-lookup "check for reference match" icon button - never calls a real provider, only used against pre-seeded/already-resolved data. /// diff --git a/test/BlazorApp.PlaywrightTests/Smoke/BookSmokeTest.cs b/test/BlazorApp.PlaywrightTests/Smoke/BookSmokeTest.cs index 5d0af27b..6f0f6e74 100644 --- a/test/BlazorApp.PlaywrightTests/Smoke/BookSmokeTest.cs +++ b/test/BlazorApp.PlaywrightTests/Smoke/BookSmokeTest.cs @@ -30,7 +30,7 @@ public async Task AddEditAndDelete_BookThroughTheList() await detail.WaitForReadyAsync(); await Assertions.Expect(detail.TitleInput).ToHaveValueAsync(title); - await BookDetailPage.SetFieldAsync(detail.SeriesInput, Series); + await DetailPageBase.SetFieldAsync(detail.SeriesInput, Series); // Round-tripping via the list (rather than a raw page reload) proves the edit persisted server-side just as well - // BookDetail.razor re-fetches via GetOneAsync on every navigation to it - diff --git a/test/BlazorApp.PlaywrightTests/Smoke/CarSmokeTest.cs b/test/BlazorApp.PlaywrightTests/Smoke/CarSmokeTest.cs index b7fcd2d9..adf28031 100644 --- a/test/BlazorApp.PlaywrightTests/Smoke/CarSmokeTest.cs +++ b/test/BlazorApp.PlaywrightTests/Smoke/CarSmokeTest.cs @@ -17,6 +17,7 @@ public async Task AddAndDelete_CarThroughTheList() SkipIfReadOnly(); var name = $"E2e Smoke Car {Guid.NewGuid():N}"; + const string imageUrl = "https://picsum.photos/seed/e2e-car/600/300"; var home = await new HomePage(Page).OpenAsync(); var list = await home.OpenCarsAsync(); @@ -28,7 +29,14 @@ public async Task AddAndDelete_CarThroughTheList() await detail.WaitForReadyAsync(); await Assertions.Expect(detail.TitleInput).ToHaveValueAsync(name); + // cover image: the detail banner and the list's wide thumbnail (same shape as VideoGames.razor) + // must both pick up the URL - a plain default-portrait thumb was a reported regression here. + await DetailPageBase.SetFieldAsync(detail.ImageUrlInput, imageUrl); + await Assertions.Expect(detail.CoverImage).ToHaveAttributeAsync("src", imageUrl); + list = await detail.OpenCarsAsync(); + await Assertions.Expect(list.Row(name).Locator(".kt-item-thumb.wide img")).ToHaveAttributeAsync("src", imageUrl); + await list.DeleteAsync(name); await Assertions.Expect(list.Row(name)).Not.ToBeVisibleAsync(); } diff --git a/test/BlazorApp.PlaywrightTests/Smoke/CollectibleSmokeTest.cs b/test/BlazorApp.PlaywrightTests/Smoke/CollectibleSmokeTest.cs new file mode 100644 index 00000000..9eda80ac --- /dev/null +++ b/test/BlazorApp.PlaywrightTests/Smoke/CollectibleSmokeTest.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading.Tasks; +using Keeptrack.BlazorApp.PlaywrightTests.Hosting; +using Keeptrack.BlazorApp.PlaywrightTests.Pages; +using Microsoft.Playwright; +using Xunit; + +namespace Keeptrack.BlazorApp.PlaywrightTests.Smoke; + +/// +/// Add/edit/delete coverage for the new Collectible type, same shape as . +/// Opening the detail page here is exactly the flow that regressed once already: the new type's API +/// client was added to the codebase but never registered in BlazorApp's DI container +/// (AddWebApiHttpClient), so navigating to the list and opening an item threw InvalidOperationException +/// at render time - a bug the build and every unit test missed, and only a real browser click surfaces. +/// This also covers the cover image round trip (fill the URL, see the detail banner and the list's wide +/// thumbnail both pick it up) - the other thing that shipped wrong on the first two attempts. +/// +[Trait("Category", "E2eTests")] +[Trait("Mode", "Mutating")] +public class CollectibleSmokeTest(End2EndFixture fixture) : SmokeTestBase(fixture) +{ + [Fact] + public async Task AddEditAndDelete_CollectibleThroughTheList() + { + SkipIfReadOnly(); + + var title = $"E2e Smoke Collectible {Guid.NewGuid():N}"; + const string imageUrl = "https://picsum.photos/seed/e2e-collectible/600/300"; + + var home = await new HomePage(Page).OpenAsync(); + var list = await home.OpenCollectiblesAsync(); + await list.ClickAddAsync(); + await list.FillAsync("title-input", title); + await list.SaveNewAsync(); + + var detail = new CollectibleDetailPage(Page); + await detail.WaitForReadyAsync(); + await Assertions.Expect(detail.TitleInput).ToHaveValueAsync(title); + + await DetailPageBase.SetFieldAsync(detail.ImageUrlInput, imageUrl); + await Assertions.Expect(detail.CoverImage).ToHaveAttributeAsync("src", imageUrl); + + list = await detail.OpenCollectiblesAsync(); + // ItemImageShape="wide" (same as VideoGames.razor) - a plain default-portrait thumb was the + // reported regression, so this pins the actual rendered shape, not just that an image exists. + await Assertions.Expect(list.Row(title).Locator(".kt-item-thumb.wide img")).ToHaveAttributeAsync("src", imageUrl); + + await list.DeleteAsync(title); + await Assertions.Expect(list.Row(title)).Not.ToBeVisibleAsync(); + } +} diff --git a/test/BlazorApp.PlaywrightTests/Smoke/GearSmokeTest.cs b/test/BlazorApp.PlaywrightTests/Smoke/GearSmokeTest.cs new file mode 100644 index 00000000..afc772a7 --- /dev/null +++ b/test/BlazorApp.PlaywrightTests/Smoke/GearSmokeTest.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using Keeptrack.BlazorApp.PlaywrightTests.Hosting; +using Keeptrack.BlazorApp.PlaywrightTests.Pages; +using Microsoft.Playwright; +using Xunit; + +namespace Keeptrack.BlazorApp.PlaywrightTests.Smoke; + +/// +/// Add/edit/delete coverage for the new Gear type - see for why opening +/// the detail page (not just building/unit-testing) is the point of this test. +/// +[Trait("Category", "E2eTests")] +[Trait("Mode", "Mutating")] +public class GearSmokeTest(End2EndFixture fixture) : SmokeTestBase(fixture) +{ + [Fact] + public async Task AddEditAndDelete_GearThroughTheList() + { + SkipIfReadOnly(); + + var title = $"E2e Smoke Gear {Guid.NewGuid():N}"; + const string imageUrl = "https://picsum.photos/seed/e2e-gear/600/300"; + + var home = await new HomePage(Page).OpenAsync(); + var list = await home.OpenGearAsync(); + await list.ClickAddAsync(); + await list.FillAsync("title-input", title); + await list.SaveNewAsync(); + + var detail = new GearDetailPage(Page); + await detail.WaitForReadyAsync(); + await Assertions.Expect(detail.TitleInput).ToHaveValueAsync(title); + + await DetailPageBase.SetFieldAsync(detail.ImageUrlInput, imageUrl); + await Assertions.Expect(detail.CoverImage).ToHaveAttributeAsync("src", imageUrl); + + list = await detail.OpenGearAsync(); + // ItemImageShape="wide" (same as VideoGames.razor) - a plain default-portrait thumb was the + // reported regression, so this pins the actual rendered shape, not just that an image exists. + await Assertions.Expect(list.Row(title).Locator(".kt-item-thumb.wide img")).ToHaveAttributeAsync("src", imageUrl); + + await list.DeleteAsync(title); + await Assertions.Expect(list.Row(title)).Not.ToBeVisibleAsync(); + } +} diff --git a/test/BlazorApp.PlaywrightTests/Smoke/HealthSmokeTest.cs b/test/BlazorApp.PlaywrightTests/Smoke/HealthSmokeTest.cs index d3ab9ffc..e68340ea 100644 --- a/test/BlazorApp.PlaywrightTests/Smoke/HealthSmokeTest.cs +++ b/test/BlazorApp.PlaywrightTests/Smoke/HealthSmokeTest.cs @@ -33,6 +33,12 @@ public async Task AddProfileAndUnbalancedAppointment_ThenDelete() await detail.WaitForReadyAsync(); await Assertions.Expect(detail.TitleInput).ToHaveValueAsync(name); + // cover image: HealthProfile keeps the default portrait list shape (a person's photo, unlike + // Car/House/Collectible/Gear's "wide" objects/spaces) - just the detail banner round trip here. + const string imageUrl = "https://picsum.photos/seed/e2e-health/300/400"; + await DetailPageBase.SetFieldAsync(detail.ImageUrlInput, imageUrl); + await Assertions.Expect(detail.CoverImage).ToHaveAttributeAsync("src", imageUrl); + // add an appointment paid 60 with nothing reimbursed yet - it must come back flagged await Page.GetByRole(AriaRole.Button, new PageGetByRoleOptions { Name = "+ Add entry" }).ClickAsync(); // data-testid, not GetByLabel: the modal's label/input pairs have no for/id association (the diff --git a/test/BlazorApp.PlaywrightTests/Smoke/HouseSmokeTest.cs b/test/BlazorApp.PlaywrightTests/Smoke/HouseSmokeTest.cs index 27cd81be..667625d8 100644 --- a/test/BlazorApp.PlaywrightTests/Smoke/HouseSmokeTest.cs +++ b/test/BlazorApp.PlaywrightTests/Smoke/HouseSmokeTest.cs @@ -17,6 +17,7 @@ public async Task AddAndDelete_HouseThroughTheList() SkipIfReadOnly(); var name = $"E2e Smoke House {Guid.NewGuid():N}"; + const string imageUrl = "https://picsum.photos/seed/e2e-house/600/300"; var home = await new HomePage(Page).OpenAsync(); var list = await home.OpenHousesAsync(); @@ -28,7 +29,14 @@ public async Task AddAndDelete_HouseThroughTheList() await detail.WaitForReadyAsync(); await Assertions.Expect(detail.TitleInput).ToHaveValueAsync(name); + // cover image: the detail banner and the list's wide thumbnail (same shape as VideoGames.razor) + // must both pick up the URL - a plain default-portrait thumb was a reported regression here. + await DetailPageBase.SetFieldAsync(detail.ImageUrlInput, imageUrl); + await Assertions.Expect(detail.CoverImage).ToHaveAttributeAsync("src", imageUrl); + list = await detail.OpenHousesAsync(); + await Assertions.Expect(list.Row(name).Locator(".kt-item-thumb.wide img")).ToHaveAttributeAsync("src", imageUrl); + await list.DeleteAsync(name); await Assertions.Expect(list.Row(name)).Not.ToBeVisibleAsync(); } diff --git a/test/BlazorApp.PlaywrightTests/Smoke/MobileScreenshotTest.cs b/test/BlazorApp.PlaywrightTests/Smoke/MobileScreenshotTest.cs index 1d8196d6..e460781f 100644 --- a/test/BlazorApp.PlaywrightTests/Smoke/MobileScreenshotTest.cs +++ b/test/BlazorApp.PlaywrightTests/Smoke/MobileScreenshotTest.cs @@ -89,7 +89,10 @@ public async Task CaptureAllPagesAtPhoneViewport() await CaptureFirstDetailAsync("/cars", "car-detail"); await CaptureFirstDetailAsync("/books", "book-detail"); await CaptureFirstDetailAsync("/health", "health-detail"); - await CaptureFirstDetailAsync("/video-games", "video-game-detail"); + // targeted by title (not "first") so these two land on the CustomImageUrl-seeded items specifically, + // not whichever item happens to sort first in a list that grows over time + await CaptureDetailByTitleAsync("/video-games", "Hades", "video-game-detail"); + await CaptureDetailByTitleAsync("/albums", "Nevermind", "album-detail"); // The Add form modal on a list page. await Page.GotoAsync("/movies"); @@ -236,7 +239,10 @@ private static async Task SeedAsync(HttpClient api, List created) Artist = "Nirvana", Year = 1991, Rating = 4.5f, - IsFavorite = true + IsFavorite = true, + // proves the CustomImageUrl override shows up on both the list thumbnail and the detail cover, + // taking priority over the linked Discogs cover it's about to be linked to below + CustomImageUrl = "https://picsum.photos/seed/nevermind-custom-cover/400/400" }); await CreateAsync(api, created, "api/video-games", new VideoGameDto { @@ -244,7 +250,10 @@ private static async Task SeedAsync(HttpClient api, List created) Year = 2020, Rating = 4.5f, // a game's copies are its platform entries - there is no separate owned flag anymore - Platforms = [new VideoGamePlatformDto { Platform = "PC", CopyType = CopyType.Digital, State = "Current" }] + Platforms = [new VideoGamePlatformDto { Platform = "PC", CopyType = CopyType.Digital, State = "Current" }], + // proves the CustomImageUrl override shows up on both the list thumbnail and the detail cover, + // taking priority over the linked RAWG cover it's about to be linked to below + CustomImageUrl = "https://picsum.photos/seed/hades-custom-cover/600/300" }); await LinkFirstCandidateAsync(api, ReferenceItemType.Album, "Nevermind", 1991, "Nirvana"); await LinkFirstCandidateAsync(api, ReferenceItemType.VideoGame, "Hades", 2020, null); @@ -387,4 +396,19 @@ private async Task CaptureFirstDetailAsync(string listRoute, string name) await Page.WaitForTimeoutAsync(1500); await Page.ScreenshotAsync(new PageScreenshotOptions { Path = Path.Combine(ShotsDirectory, $"{name}.png"), FullPage = true }); } + + private async Task CaptureDetailByTitleAsync(string listRoute, string title, string name) + { + await Page.GotoAsync(listRoute); + await Page.WaitForTimeoutAsync(1200); + var itemLink = Page.Locator($"main a[href^='{listRoute}/']", new PageLocatorOptions { HasText = title }).First; + if (await itemLink.CountAsync() == 0) + { + return; + } + + await itemLink.ClickAsync(); + await Page.WaitForTimeoutAsync(1500); + await Page.ScreenshotAsync(new PageScreenshotOptions { Path = Path.Combine(ShotsDirectory, $"{name}.png"), FullPage = true }); + } } diff --git a/test/BlazorApp.PlaywrightTests/Smoke/OwnershipSmokeTest.cs b/test/BlazorApp.PlaywrightTests/Smoke/OwnershipSmokeTest.cs index dc65938e..06e0c88e 100644 --- a/test/BlazorApp.PlaywrightTests/Smoke/OwnershipSmokeTest.cs +++ b/test/BlazorApp.PlaywrightTests/Smoke/OwnershipSmokeTest.cs @@ -41,10 +41,10 @@ public async Task AddingAndRemovingAnOwnedVersion_DrivesTheOwnedState() // add a version (defaults to Physical), fill in its purchase details, and save the draft await Page.GetByRole(AriaRole.Button, new PageGetByRoleOptions { Name = "+ Add version" }).ClickAsync(); - await BookDetailPage.SetFieldAsync(Page.GetByTestId("version-price-input"), "12.50"); - await BookDetailPage.SetFieldAsync(Page.GetByTestId("version-acquired-input"), "2024-05-17"); - await BookDetailPage.SetFieldAsync(Page.GetByTestId("version-vendor-input"), "E2e Bookshop"); - await BookDetailPage.SetFieldAsync(Page.GetByTestId("version-reference-input"), "Paperback 2nd edition"); + await DetailPageBase.SetFieldAsync(Page.GetByTestId("version-price-input"), "12.50"); + await DetailPageBase.SetFieldAsync(Page.GetByTestId("version-acquired-input"), "2024-05-17"); + await DetailPageBase.SetFieldAsync(Page.GetByTestId("version-vendor-input"), "E2e Bookshop"); + await DetailPageBase.SetFieldAsync(Page.GetByTestId("version-reference-input"), "Paperback 2nd edition"); await Page.GetByTestId("version-save-button").ClickAsync(); // round-trip via the list: the row must show the derived Owned badge, and reopening the diff --git a/test/BlazorApp.PlaywrightTests/Smoke/QuickAddSmokeTest.cs b/test/BlazorApp.PlaywrightTests/Smoke/QuickAddSmokeTest.cs index 6c312afe..4064d0ab 100644 --- a/test/BlazorApp.PlaywrightTests/Smoke/QuickAddSmokeTest.cs +++ b/test/BlazorApp.PlaywrightTests/Smoke/QuickAddSmokeTest.cs @@ -45,9 +45,9 @@ public async Task QuickAddMovie_WithAnOwnedCopy_LandsOnItsDetailPageWithTheCopyS var quickAdd = await home.OpenQuickAddAsync(); await quickAdd.SelectTypeAsync("movie"); - await BookDetailPage.SetFieldAsync(quickAdd.TitleInput, title); + await DetailPageBase.SetFieldAsync(quickAdd.TitleInput, title); await Page.GetByRole(AriaRole.Button, new PageGetByRoleOptions { Name = "I own a copy" }).ClickAsync(); - await BookDetailPage.SetFieldAsync(Page.GetByTestId("version-price-input"), "19.99"); + await DetailPageBase.SetFieldAsync(Page.GetByTestId("version-price-input"), "19.99"); await quickAdd.SaveButton.ClickAsync(); var detail = new MovieDetailPage(Page); @@ -82,8 +82,8 @@ public async Task QuickAddCarRecord_WithASingleExistingCar_PreselectsItAndSavesT await quickAdd.SelectTypeAsync("car"); // the tenant now has exactly one car - it's preselected silently, no segmented picker to click - await BookDetailPage.SetFieldAsync(Page.GetByTestId("mileage-input"), mileage.ToString()); - await BookDetailPage.SetFieldAsync(Page.GetByTestId("cost-input"), "65.40"); + await DetailPageBase.SetFieldAsync(Page.GetByTestId("mileage-input"), mileage.ToString()); + await DetailPageBase.SetFieldAsync(Page.GetByTestId("cost-input"), "65.40"); await quickAdd.SaveButton.ClickAsync(); var detail = new CarDetailPage(Page); diff --git a/test/BlazorApp.UnitTests/Components/Shared/DateOnlyInputTest.cs b/test/BlazorApp.UnitTests/Components/Shared/DateOnlyInputTest.cs new file mode 100644 index 00000000..77645710 --- /dev/null +++ b/test/BlazorApp.UnitTests/Components/Shared/DateOnlyInputTest.cs @@ -0,0 +1,25 @@ +using System; +using AwesomeAssertions; +using Keeptrack.BlazorApp.Components.Shared; +using Xunit; + +namespace Keeptrack.BlazorApp.UnitTests.Components.Shared; + +[Trait("Category", "UnitTests")] +public class DateOnlyInputTest +{ + [Fact] + public void Parse_ParsesAnHtmlDateInputValue_RegardlessOfCurrentCulture() + { + DateOnlyInput.Parse("2026-07-23").Should().Be(new DateOnly(2026, 7, 23)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("not-a-date")] + public void Parse_ReturnsNull_ForMissingOrInvalidInput(object? value) + { + DateOnlyInput.Parse(value).Should().BeNull(); + } +} diff --git a/test/WebApi.IntegrationTests/Resources/AlbumResourceTest.cs b/test/WebApi.IntegrationTests/Resources/AlbumResourceTest.cs index e9f8112b..6ff7371c 100644 --- a/test/WebApi.IntegrationTests/Resources/AlbumResourceTest.cs +++ b/test/WebApi.IntegrationTests/Resources/AlbumResourceTest.cs @@ -1,11 +1,17 @@ +using System; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; using AwesomeAssertions; using Bogus; using Keeptrack.Common.System; +using Keeptrack.Domain.Repositories; +using Keeptrack.Infrastructure.MongoDb.Entities; using Keeptrack.WebApi.Contracts.Dto; using Keeptrack.WebApi.IntegrationTests.Hosting; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; using Xunit; namespace Keeptrack.WebApi.IntegrationTests.Resources; @@ -28,7 +34,15 @@ public async Task AlbumResourceFullCycle_IsOk() await Authenticate(); var input = new Faker() - .Rules((f, o) => { o.Artist = f.Random.AlphaNumeric(8); o.Title = f.Random.AlphaNumeric(14); }) + .Rules((f, o) => + { + o.Artist = f.Random.AlphaNumeric(8); + o.Title = f.Random.AlphaNumeric(14); + // round-trips CustomImageUrl through the real Mapperly mappers + MongoDB - a plain scalar field + // with no special mapping, but a real integration test is what would catch a missed + // [BsonElement]/mapper ignore, not a mocked unit test. + o.CustomImageUrl = f.Internet.Url(); + }) .Generate(); var created = await PostAsync($"/{ResourceEndpoint}", input); created.Id.Should().NotBeNullOrEmpty(); @@ -88,7 +102,7 @@ public async Task AlbumResourceOwnedFilter_OnlyReturnsAlbumsWithOwnedVersions_Is Title = title, Artist = "Owned Filter Artist", // "owned" is derived from having at least one owned version, not a stored flag - OwnedVersions = [new OwnedVersionDto { CopyType = CopyType.Physical, Price = 24.50m, Vendor = "Record store", Reference = "Vinyl reissue" }] + OwnedVersions = [new OwnedVersionDto { CopyType = CopyType.Physical, Price = 24.50m, Vendor = "Record store", Reference = "Vinyl reissue", ProductName = "Deluxe vinyl edition" }] }); var notOwned = await PostAsync($"/{ResourceEndpoint}", new AlbumDto { Title = title, Artist = "Owned Filter Artist" }); @@ -127,4 +141,48 @@ public async Task AlbumResourceSearch_FiltersToMatchingTitleOrArtist_IsOk() await DeleteAsync($"/{ResourceEndpoint}/{created.Id}"); } } + + /// + /// follows 's exact + /// shape - AlbumController.OnListMappedAsync is expected to apply it over the linked reference's own + /// cover on every list read. + /// + [Fact] + public async Task AlbumResourceList_CustomImageUrlOverridesTheLinkedReferencesCover() + { + using var scope = Factory.Services.CreateScope(); + var referenceRepository = scope.ServiceProvider.GetRequiredService(); + var uniqueTitle = $"CustomImageOverrideTarget-{Guid.NewGuid():N}"; + + var reference = await referenceRepository.UpsertAsync(new Keeptrack.Domain.Models.AlbumReferenceModel + { + Title = "Some Reference Title", + TitleNormalized = "some reference title", + ExternalIds = new Dictionary { ["discogs"] = $"discogs-{Guid.NewGuid():N}" }, + ImageUrl = "https://example.com/reference-cover.jpg" + }); + + await Authenticate(); + const string customImageUrl = "https://example.com/custom-cover.jpg"; + var created = await PostAsync($"/{ResourceEndpoint}", new AlbumDto + { + Title = uniqueTitle, + Artist = "Some Artist", + ReferenceId = reference.Id, + CustomImageUrl = customImageUrl + }); + + try + { + var list = await GetAsync>($"/{ResourceEndpoint}?search={uniqueTitle}"); + var item = list.Items.Should().ContainSingle(x => x.Id == created.Id).Subject; + item.ImageUrl.Should().Be(customImageUrl); + } + finally + { + await DeleteAsync($"/{ResourceEndpoint}/{created.Id}"); + var referenceCollection = scope.ServiceProvider.GetRequiredService().GetCollection("album_reference"); + await referenceCollection.DeleteOneAsync(Builders.Filter.Eq(x => x.Id, reference.Id), TestContext.Current.CancellationToken); + } + } } diff --git a/test/WebApi.IntegrationTests/Resources/AmazonFixtureCsvBuilder.cs b/test/WebApi.IntegrationTests/Resources/AmazonFixtureCsvBuilder.cs new file mode 100644 index 00000000..be721cb6 --- /dev/null +++ b/test/WebApi.IntegrationTests/Resources/AmazonFixtureCsvBuilder.cs @@ -0,0 +1,64 @@ +using System.Text; + +namespace Keeptrack.WebApi.IntegrationTests.Resources; + +/// +/// Builds a small, synthetic Amazon order-history CSV for tests - never use a real personal export as a +/// test fixture (same rule as ). Amazon's export has no category +/// column, so the Movie/TvShow/VideoGame rows below are just ordinary non-ISBN order lines - the test +/// itself is what assigns their media type at commit time, exactly like a real user would in the review UI. +/// +internal static class AmazonFixtureCsvBuilder +{ + // Deliberately namespaced "Amazon Import" (not the generic "Keeptrack Integration Test X" TvTimeFixtureZipBuilder + // uses) - MovieTitle once collided exactly with TvTimeFixtureZipBuilder.MovieTitle in the shared test database, + // which made TvTimeImportResourceTest's title-matching pick up this fixture's leftover/concurrent movie and + // report 0 created instead of 1. Keep every title here distinct from every other fixture's, not just internally + // consistent. + public const string BookTitle = "Keeptrack Amazon Import Test Book"; + + /// A real, checksum-valid ISBN-10 - exercises the "looks like a book" heuristic honestly. + public const string BookIsbn = "0552177571"; + + public const string BookOrderId = "999-1111111-1111111"; + + public const string NonBookTitle = "Keeptrack Amazon Import Test Gadget"; + private const string NonBookAsin = "B000000001"; + private const string NonBookOrderId = "999-2222222-2222222"; + + public const string MovieTitle = "Keeptrack Amazon Import Test Movie"; + private const string MovieAsin = "B000000002"; + public const string MovieOrderId = "999-3333333-3333333"; + + public const string TvShowTitle = "Keeptrack Amazon Import Test TV Show"; + private const string TvShowAsin = "B000000003"; + public const string TvShowOrderId = "999-4444444-4444444"; + + public const string VideoGameTitle = "Keeptrack Amazon Import Test Video Game"; + private const string VideoGameAsin = "B000000004"; + public const string VideoGameOrderId = "999-5555555-5555555"; + + public const string GearTitle = "Keeptrack Amazon Import Test Gear"; + private const string GearAsin = "B000000005"; + public const string GearOrderId = "999-8888888-8888888"; + + public const string CollectibleTitle = "Keeptrack Amazon Import Test Collectible"; + private const string CollectibleAsin = "B000000006"; + public const string CollectibleOrderId = "999-9999999-9999999"; + + public static byte[] Build() + { + var csv = $""" + ASIN,Order Date,Order ID,Product Name,Product Condition,Total Amount,Website + {BookIsbn},2024-01-24T09:01:58Z,{BookOrderId},{BookTitle},New,10.49,Amazon.fr + {NonBookAsin},2020-06-13T21:11:47Z,{NonBookOrderId},{NonBookTitle},New,14.99,Amazon.fr + {MovieAsin},2019-05-01T10:00:00Z,{MovieOrderId},{MovieTitle},New,12.99,Amazon.fr + {TvShowAsin},2018-03-15T10:00:00Z,{TvShowOrderId},{TvShowTitle},New,29.99,Amazon.fr + {VideoGameAsin},2021-07-20T10:00:00Z,{VideoGameOrderId},{VideoGameTitle},New,49.99,Amazon.fr + {GearAsin},2022-02-10T10:00:00Z,{GearOrderId},{GearTitle},New,89.99,Amazon.fr + {CollectibleAsin},2023-11-05T10:00:00Z,{CollectibleOrderId},{CollectibleTitle},New,39.99,Amazon.fr + + """; + return Encoding.UTF8.GetBytes(csv); + } +} diff --git a/test/WebApi.IntegrationTests/Resources/AmazonImportResourceTest.cs b/test/WebApi.IntegrationTests/Resources/AmazonImportResourceTest.cs new file mode 100644 index 00000000..43089d3c --- /dev/null +++ b/test/WebApi.IntegrationTests/Resources/AmazonImportResourceTest.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using AwesomeAssertions; +using Keeptrack.Common.System; +using Keeptrack.WebApi.Contracts.Dto; +using Keeptrack.WebApi.IntegrationTests.Hosting; +using Xunit; + +namespace Keeptrack.WebApi.IntegrationTests.Resources; + +public class AmazonImportResourceTest(KestrelWebAppFactory factory) + : ResourceTestBase(factory) +{ + [Fact] + public async Task PreviewThenCommit_CreatesOneItemPerMediaType_AndFlagsThemAllAlreadyImportedOnReimport() + { + await Authenticate(); + + var csv = AmazonFixtureCsvBuilder.Build(); + + var preview = await PostFileAsync>("/api/import/amazon/preview", "file", csv, "orders.csv"); + var bookRow = preview.Should().Contain(r => r.Title == AmazonFixtureCsvBuilder.BookTitle).Subject; + bookRow.LooksLikeBook.Should().BeTrue(); + bookRow.SuggestedIsbn.Should().Be(AmazonFixtureCsvBuilder.BookIsbn); + bookRow.AlreadyImported.Should().BeFalse(); + + var nonBookRow = preview.Should().Contain(r => r.Title == AmazonFixtureCsvBuilder.NonBookTitle).Subject; + nonBookRow.LooksLikeBook.Should().BeFalse(); + + var movieRow = preview.Should().Contain(r => r.Title == AmazonFixtureCsvBuilder.MovieTitle).Subject; + var tvShowRow = preview.Should().Contain(r => r.Title == AmazonFixtureCsvBuilder.TvShowTitle).Subject; + var videoGameRow = preview.Should().Contain(r => r.Title == AmazonFixtureCsvBuilder.VideoGameTitle).Subject; + var gearRow = preview.Should().Contain(r => r.Title == AmazonFixtureCsvBuilder.GearTitle).Subject; + var collectibleRow = preview.Should().Contain(r => r.Title == AmazonFixtureCsvBuilder.CollectibleTitle).Subject; + // none of these have an ISBN-shaped ASIN - confirms the heuristic never mistakes them for books + movieRow.LooksLikeBook.Should().BeFalse(); + tvShowRow.LooksLikeBook.Should().BeFalse(); + videoGameRow.LooksLikeBook.Should().BeFalse(); + gearRow.LooksLikeBook.Should().BeFalse(); + collectibleRow.LooksLikeBook.Should().BeFalse(); + + try + { + // a video game row with no platform must be rejected before anything is persisted + var invalidPlatformRequest = new AmazonImportCommitRequestDto { Items = [ToCommitItem(videoGameRow, AmazonImportMediaType.VideoGame, platform: null)] }; + await PostAsync("/api/import/amazon/commit", invalidPlatformRequest, HttpStatusCode.BadRequest); + + // a row with no media type chosen must also be rejected before anything is persisted + var noMediaTypeItem = ToCommitItem(bookRow, AmazonImportMediaType.Book); + noMediaTypeItem.MediaType = null; + var invalidTypeRequest = new AmazonImportCommitRequestDto { Items = [noMediaTypeItem] }; + await PostAsync("/api/import/amazon/commit", invalidTypeRequest, HttpStatusCode.BadRequest); + + var commitRequest = new AmazonImportCommitRequestDto + { + Items = + [ + ToCommitItem(bookRow, AmazonImportMediaType.Book, isbn: bookRow.SuggestedIsbn, year: 1997), + ToCommitItem(movieRow, AmazonImportMediaType.Movie), + ToCommitItem(tvShowRow, AmazonImportMediaType.TvShow), + ToCommitItem(videoGameRow, AmazonImportMediaType.VideoGame, platform: "PS5"), + ToCommitItem(gearRow, AmazonImportMediaType.Gear), + ToCommitItem(collectibleRow, AmazonImportMediaType.Collectible) + ] + }; + + var commitResult = await PostAsync("/api/import/amazon/commit", commitRequest); + commitResult.BooksCreated.Should().Be(1); + commitResult.MoviesCreated.Should().Be(1); + commitResult.TvShowsCreated.Should().Be(1); + commitResult.VideoGamesCreated.Should().Be(1); + commitResult.GearCreated.Should().Be(1); + commitResult.CollectiblesCreated.Should().Be(1); + + var books = await GetAsync>($"/api/books?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.BookTitle)}"); + var book = books.Items.Should().ContainSingle().Subject; + book.Year.Should().Be(1997); + book.Isbn.Should().Be(AmazonFixtureCsvBuilder.BookIsbn); + book.OwnedVersions.Should().ContainSingle(); + book.OwnedVersions[0].Price.Should().Be(10.49m); + book.OwnedVersions[0].Reference.Should().Contain(AmazonFixtureCsvBuilder.BookOrderId); + book.Notes.Should().Be($"Title from Amazon: {AmazonFixtureCsvBuilder.BookTitle}\nISBN from Amazon: {AmazonFixtureCsvBuilder.BookIsbn}"); + + var movies = await GetAsync>($"/api/movies?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.MovieTitle)}"); + var movie = movies.Items.Should().ContainSingle().Subject; + movie.OwnedVersions.Should().ContainSingle(); + movie.Notes.Should().Be($"Title from Amazon: {AmazonFixtureCsvBuilder.MovieTitle}"); + + var tvShows = await GetAsync>($"/api/tv-shows?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.TvShowTitle)}"); + var tvShow = tvShows.Items.Should().ContainSingle().Subject; + tvShow.OwnedVersions.Should().ContainSingle(); + + var videoGames = await GetAsync>($"/api/video-games?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.VideoGameTitle)}"); + var videoGame = videoGames.Items.Should().ContainSingle().Subject; + videoGame.Platforms.Should().ContainSingle(); + videoGame.Platforms[0].Platform.Should().Be("PS5"); + videoGame.Platforms[0].Reference.Should().Contain(AmazonFixtureCsvBuilder.VideoGameOrderId); + + var gearList = await GetAsync>($"/api/gear?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.GearTitle)}"); + var gear = gearList.Items.Should().ContainSingle().Subject; + gear.OwnedVersions.Should().ContainSingle(); + gear.OwnedVersions[0].Reference.Should().Contain(AmazonFixtureCsvBuilder.GearOrderId); + + var collectibles = await GetAsync>($"/api/collectibles?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.CollectibleTitle)}"); + var collectible = collectibles.Items.Should().ContainSingle().Subject; + collectible.OwnedVersions.Should().ContainSingle(); + collectible.OwnedVersions[0].Reference.Should().Contain(AmazonFixtureCsvBuilder.CollectibleOrderId); + + // re-preview after commit: every just-imported order must now be flagged, regardless of which + // type it was imported as, so re-uploading a newer export later doesn't silently duplicate any of them + var secondPreview = await PostFileAsync>("/api/import/amazon/preview", "file", csv, "orders.csv"); + secondPreview.Should().Contain(r => r.Title == AmazonFixtureCsvBuilder.BookTitle && r.AlreadyImported); + secondPreview.Should().Contain(r => r.Title == AmazonFixtureCsvBuilder.MovieTitle && r.AlreadyImported); + secondPreview.Should().Contain(r => r.Title == AmazonFixtureCsvBuilder.TvShowTitle && r.AlreadyImported); + secondPreview.Should().Contain(r => r.Title == AmazonFixtureCsvBuilder.VideoGameTitle && r.AlreadyImported); + secondPreview.Should().Contain(r => r.Title == AmazonFixtureCsvBuilder.GearTitle && r.AlreadyImported); + secondPreview.Should().Contain(r => r.Title == AmazonFixtureCsvBuilder.CollectibleTitle && r.AlreadyImported); + + // committing the exact same rows again (e.g. the user re-runs the import without noticing the + // "already imported" badge) must not duplicate anything - this is the bug reported in practice + var secondCommitResult = await PostAsync("/api/import/amazon/commit", commitRequest); + secondCommitResult.BooksCreated.Should().Be(0); + secondCommitResult.BooksMergedInto.Should().Be(0); + secondCommitResult.BooksSkipped.Should().Be(1); + secondCommitResult.MoviesSkipped.Should().Be(1); + secondCommitResult.TvShowsSkipped.Should().Be(1); + secondCommitResult.VideoGamesSkipped.Should().Be(1); + secondCommitResult.GearSkipped.Should().Be(1); + secondCommitResult.CollectiblesSkipped.Should().Be(1); + + var booksAfterReimport = await GetAsync>($"/api/books?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.BookTitle)}"); + booksAfterReimport.Items.Should().ContainSingle().Which.OwnedVersions.Should().ContainSingle(); + + var videoGamesAfterReimport = await GetAsync>($"/api/video-games?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.VideoGameTitle)}"); + videoGamesAfterReimport.Items.Should().ContainSingle().Which.Platforms.Should().ContainSingle(); + + var gearAfterReimport = await GetAsync>($"/api/gear?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.GearTitle)}"); + gearAfterReimport.Items.Should().ContainSingle().Which.OwnedVersions.Should().ContainSingle(); + + var collectiblesAfterReimport = await GetAsync>($"/api/collectibles?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.CollectibleTitle)}"); + collectiblesAfterReimport.Items.Should().ContainSingle().Which.OwnedVersions.Should().ContainSingle(); + } + finally + { + var books = await GetAsync>($"/api/books?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.BookTitle)}"); + foreach (var book in books.Items.Where(b => b.Id is not null)) + { + await DeleteAsync($"/api/books/{book.Id}"); + } + + var movies = await GetAsync>($"/api/movies?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.MovieTitle)}"); + foreach (var movie in movies.Items.Where(m => m.Id is not null)) + { + await DeleteAsync($"/api/movies/{movie.Id}"); + } + + var tvShows = await GetAsync>($"/api/tv-shows?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.TvShowTitle)}"); + foreach (var tvShow in tvShows.Items.Where(t => t.Id is not null)) + { + await DeleteAsync($"/api/tv-shows/{tvShow.Id}"); + } + + var videoGames = await GetAsync>($"/api/video-games?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.VideoGameTitle)}"); + foreach (var videoGame in videoGames.Items.Where(g => g.Id is not null)) + { + await DeleteAsync($"/api/video-games/{videoGame.Id}"); + } + + var gearList = await GetAsync>($"/api/gear?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.GearTitle)}"); + foreach (var gear in gearList.Items.Where(g => g.Id is not null)) + { + await DeleteAsync($"/api/gear/{gear.Id}"); + } + + var collectibles = await GetAsync>($"/api/collectibles?search={Uri.EscapeDataString(AmazonFixtureCsvBuilder.CollectibleTitle)}"); + foreach (var collectible in collectibles.Items.Where(c => c.Id is not null)) + { + await DeleteAsync($"/api/collectibles/{collectible.Id}"); + } + } + } + + private static AmazonImportCommitItemDto ToCommitItem(AmazonOrderPreviewRowDto row, AmazonImportMediaType mediaType, int? year = null, string? isbn = null, string? platform = null) => new() + { + RowId = row.RowId, + OrderId = row.OrderId, + Asin = row.Asin, + Title = row.Title, + AmazonTitle = row.Title, + MediaType = mediaType, + Year = year, + Isbn = isbn, + Platform = platform, + AcquiredAt = row.OrderDate, + Price = row.Price, + Vendor = row.Vendor, + CopyType = CopyType.Physical + }; + + [Fact] + public async Task Commit_ImportsBothItems_WhenTwoDifferentBooksShareTheSameOrder() + { + // reproduces a real bug: an Amazon order commonly contains several different line items, but the + // owned-copy Reference used to be built from the order id alone - so the second book from the same + // order was silently skipped as a "duplicate" of the first + await Authenticate(); + + const string sharedOrderId = "999-6666666-6666666"; + const string firstTitle = "Keeptrack Amazon Import Test Multi-Item Book A"; + const string secondTitle = "Keeptrack Amazon Import Test Multi-Item Book B"; + + var request = new AmazonImportCommitRequestDto + { + Items = + [ + new AmazonImportCommitItemDto + { + RowId = $"{sharedOrderId}:1111111111", OrderId = sharedOrderId, Asin = "1111111111", + Title = firstTitle, AmazonTitle = firstTitle, MediaType = AmazonImportMediaType.Book, CopyType = CopyType.Physical + }, + new AmazonImportCommitItemDto + { + RowId = $"{sharedOrderId}:2222222222", OrderId = sharedOrderId, Asin = "2222222222", + Title = secondTitle, AmazonTitle = secondTitle, MediaType = AmazonImportMediaType.Book, CopyType = CopyType.Physical + } + ] + }; + + try + { + var commitResult = await PostAsync("/api/import/amazon/commit", request); + commitResult.BooksCreated.Should().Be(2); + commitResult.BooksSkipped.Should().Be(0); + + var firstBook = (await GetAsync>($"/api/books?search={Uri.EscapeDataString(firstTitle)}")).Items.Should().ContainSingle().Subject; + var secondBook = (await GetAsync>($"/api/books?search={Uri.EscapeDataString(secondTitle)}")).Items.Should().ContainSingle().Subject; + firstBook.OwnedVersions.Should().ContainSingle().Which.Reference.Should().Contain("1111111111"); + secondBook.OwnedVersions.Should().ContainSingle().Which.Reference.Should().Contain("2222222222"); + } + finally + { + foreach (var title in new[] { firstTitle, secondTitle }) + { + var books = await GetAsync>($"/api/books?search={Uri.EscapeDataString(title)}"); + foreach (var book in books.Items.Where(b => b.Id is not null)) + { + await DeleteAsync($"/api/books/{book.Id}"); + } + } + } + } +} diff --git a/test/WebApi.IntegrationTests/Resources/CarResourceTest.cs b/test/WebApi.IntegrationTests/Resources/CarResourceTest.cs index a60aebc6..32646e1c 100644 --- a/test/WebApi.IntegrationTests/Resources/CarResourceTest.cs +++ b/test/WebApi.IntegrationTests/Resources/CarResourceTest.cs @@ -36,6 +36,7 @@ public async Task CarResourceFullCycle_IsOk() o.Year = f.Random.Int(1990, 2024); o.LicensePlate = f.Random.AlphaNumeric(8); o.EnergyType = CarEnergyType.Combustion; + o.ImageUrl = f.Internet.Url(); }) .Generate(); var created = await PostAsync($"/{ResourceEndpoint}", input); diff --git a/test/WebApi.IntegrationTests/Resources/CollectibleResourceTest.cs b/test/WebApi.IntegrationTests/Resources/CollectibleResourceTest.cs new file mode 100644 index 00000000..9c3d6ae1 --- /dev/null +++ b/test/WebApi.IntegrationTests/Resources/CollectibleResourceTest.cs @@ -0,0 +1,114 @@ +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using AwesomeAssertions; +using Bogus; +using Keeptrack.Common.System; +using Keeptrack.WebApi.Contracts.Dto; +using Keeptrack.WebApi.IntegrationTests.Hosting; +using Xunit; + +namespace Keeptrack.WebApi.IntegrationTests.Resources; + +/// +/// Basic full-cycle CRUD coverage for Collectible, same shape as +/// minus reference-linking (Collectible has no external reference-data provider). +/// +public class CollectibleResourceTest(KestrelWebAppFactory factory) + : ResourceTestBase(factory) +{ + private const string ResourceEndpoint = "api/collectibles"; + + [Fact] + public async Task CollectibleResourceFullCycle_IsOk() + { + await GetAsync($"/{ResourceEndpoint}", HttpStatusCode.Unauthorized); + + await Authenticate(); + + var input = new Faker() + .Rules((f, o) => + { + o.Title = f.Random.AlphaNumeric(14); + o.Brand = f.Company.CompanyName(); + o.Year = f.Random.Int(1990, 2024); + o.Notes = f.Lorem.Sentence(); + o.ImageUrl = f.Internet.Url(); + }) + .Generate(); + var created = await PostAsync($"/{ResourceEndpoint}", input); + created.Id.Should().NotBeNullOrEmpty(); + + try + { + created.Title = "New shiny title"; + await PutAsync($"/{ResourceEndpoint}/{created.Id}", created); + + var updated = await GetAsync($"/{ResourceEndpoint}/{created.Id}"); + updated.Should().BeEquivalentTo(created); + + var finalItems = await GetAsync>($"/{ResourceEndpoint}"); + var firstItem = finalItems.Items.FirstOrDefault(x => x.Id == updated.Id); + firstItem.Should().NotBeNull(); + firstItem.Title.Should().Be(updated.Title); + } + finally + { + await DeleteAsync($"/{ResourceEndpoint}/{created.Id}"); + } + } + + [Fact] + public async Task CollectibleResourceSearch_FiltersByTitle_IsOk() + { + await Authenticate(); + + var title = System.Guid.NewGuid().ToString(); + var created = await PostAsync($"/{ResourceEndpoint}", new CollectibleDto { Title = title }); + + try + { + var results = await GetAsync>($"/{ResourceEndpoint}?search={title}"); + results.Items.Should().ContainSingle(x => x.Id == created.Id); + } + finally + { + await DeleteAsync($"/{ResourceEndpoint}/{created.Id}"); + } + } + + [Fact] + public async Task CollectibleResourceOwnedAndFavoriteFilters_OnlyReturnMatchingItems_IsOk() + { + await Authenticate(); + + var title = $"OwnedTarget-{System.Guid.NewGuid():N}"; + var owned = await PostAsync($"/{ResourceEndpoint}", new CollectibleDto + { + Title = title, + // "owned" is derived from having at least one owned version, not a stored flag + OwnedVersions = [new OwnedVersionDto { CopyType = CopyType.Physical, Price = 42.50m, ProductName = "Ultimate Collector's Edition" }] + }); + var favorite = await PostAsync($"/{ResourceEndpoint}", new CollectibleDto { Title = title, IsFavorite = true }); + var plain = await PostAsync($"/{ResourceEndpoint}", new CollectibleDto { Title = title }); + + try + { + var ownedResults = await GetAsync>($"/{ResourceEndpoint}?IsOwned=true&search={title}"); + ownedResults.Items.Should().ContainSingle(x => x.Id == owned.Id); + ownedResults.Items.Should().NotContain(x => x.Id == plain.Id); + // the version's fields must survive the full DTO -> model -> BSON round trip, including ProductName + ownedResults.Items.Single(x => x.Id == owned.Id).OwnedVersions.Should().BeEquivalentTo(owned.OwnedVersions); + + var favoriteResults = await GetAsync>($"/{ResourceEndpoint}?IsFavorite=true&search={title}"); + favoriteResults.Items.Should().ContainSingle(x => x.Id == favorite.Id); + favoriteResults.Items.Should().NotContain(x => x.Id == plain.Id); + } + finally + { + await DeleteAsync($"/{ResourceEndpoint}/{owned.Id}"); + await DeleteAsync($"/{ResourceEndpoint}/{favorite.Id}"); + await DeleteAsync($"/{ResourceEndpoint}/{plain.Id}"); + } + } +} diff --git a/test/WebApi.IntegrationTests/Resources/GearResourceTest.cs b/test/WebApi.IntegrationTests/Resources/GearResourceTest.cs new file mode 100644 index 00000000..a8c8b41d --- /dev/null +++ b/test/WebApi.IntegrationTests/Resources/GearResourceTest.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using AwesomeAssertions; +using Bogus; +using Keeptrack.Common.System; +using Keeptrack.WebApi.Contracts.Dto; +using Keeptrack.WebApi.IntegrationTests.Hosting; +using Xunit; + +namespace Keeptrack.WebApi.IntegrationTests.Resources; + +/// +/// Basic full-cycle CRUD coverage for Gear, same shape as +/// (Gear has no external reference-data provider either). +/// +public class GearResourceTest(KestrelWebAppFactory factory) + : ResourceTestBase(factory) +{ + private const string ResourceEndpoint = "api/gear"; + + [Fact] + public async Task GearResourceFullCycle_IsOk() + { + await GetAsync($"/{ResourceEndpoint}", HttpStatusCode.Unauthorized); + + await Authenticate(); + + var input = new Faker() + .Rules((f, o) => + { + o.Title = f.Random.AlphaNumeric(14); + o.Brand = f.Company.CompanyName(); + o.Year = f.Random.Int(1990, 2024); + o.Notes = f.Lorem.Sentence(); + o.ImageUrl = f.Internet.Url(); + }) + .Generate(); + var created = await PostAsync($"/{ResourceEndpoint}", input); + created.Id.Should().NotBeNullOrEmpty(); + + try + { + created.Title = "New shiny title"; + await PutAsync($"/{ResourceEndpoint}/{created.Id}", created); + + var updated = await GetAsync($"/{ResourceEndpoint}/{created.Id}"); + updated.Should().BeEquivalentTo(created); + + var finalItems = await GetAsync>($"/{ResourceEndpoint}"); + var firstItem = finalItems.Items.FirstOrDefault(x => x.Id == updated.Id); + firstItem.Should().NotBeNull(); + firstItem.Title.Should().Be(updated.Title); + } + finally + { + await DeleteAsync($"/{ResourceEndpoint}/{created.Id}"); + } + } + + [Fact] + public async Task GearResourceSearch_FiltersByTitle_IsOk() + { + await Authenticate(); + + var title = System.Guid.NewGuid().ToString(); + var created = await PostAsync($"/{ResourceEndpoint}", new GearDto { Title = title }); + + try + { + var results = await GetAsync>($"/{ResourceEndpoint}?search={title}"); + results.Items.Should().ContainSingle(x => x.Id == created.Id); + } + finally + { + await DeleteAsync($"/{ResourceEndpoint}/{created.Id}"); + } + } + + [Fact] + public async Task GearResourceCategoryFilter_OnlyReturnsMatchingItems_IsOk() + { + await Authenticate(); + + var title = $"CategoryTarget-{System.Guid.NewGuid():N}"; + var electronics = await PostAsync($"/{ResourceEndpoint}", new GearDto { Title = title, Category = "Electronics" }); + var camping = await PostAsync($"/{ResourceEndpoint}", new GearDto { Title = title, Category = "Camping" }); + var uncategorized = await PostAsync($"/{ResourceEndpoint}", new GearDto { Title = title }); + + try + { + var results = await GetAsync>($"/{ResourceEndpoint}?Category=Electronics&search={title}"); + results.Items.Should().ContainSingle(x => x.Id == electronics.Id); + results.Items.Should().NotContain(x => x.Id == camping.Id || x.Id == uncategorized.Id); + } + finally + { + await DeleteAsync($"/{ResourceEndpoint}/{electronics.Id}"); + await DeleteAsync($"/{ResourceEndpoint}/{camping.Id}"); + await DeleteAsync($"/{ResourceEndpoint}/{uncategorized.Id}"); + } + } + + [Fact] + public async Task GearCategoriesEndpoint_ReturnsDistinctSortedCategories_IsOk() + { + await Authenticate(); + + var title = $"CategoryList-{System.Guid.NewGuid():N}"; + var first = await PostAsync($"/{ResourceEndpoint}", new GearDto { Title = title, Category = "Zetatools" }); + var second = await PostAsync($"/{ResourceEndpoint}", new GearDto { Title = title, Category = "Anvils" }); + // same category twice must appear only once, and an unset category must never appear at all + var duplicate = await PostAsync($"/{ResourceEndpoint}", new GearDto { Title = title, Category = "Zetatools" }); + var uncategorized = await PostAsync($"/{ResourceEndpoint}", new GearDto { Title = title }); + + try + { + var categories = await GetAsync>($"/{ResourceEndpoint}/categories"); + categories.Should().Contain(["Anvils", "Zetatools"]); + categories.Should().OnlyHaveUniqueItems(); + var anvilsIndex = categories.IndexOf("Anvils"); + var zetatoolsIndex = categories.IndexOf("Zetatools"); + anvilsIndex.Should().BeLessThan(zetatoolsIndex, "results are sorted alphabetically"); + } + finally + { + await DeleteAsync($"/{ResourceEndpoint}/{first.Id}"); + await DeleteAsync($"/{ResourceEndpoint}/{second.Id}"); + await DeleteAsync($"/{ResourceEndpoint}/{duplicate.Id}"); + await DeleteAsync($"/{ResourceEndpoint}/{uncategorized.Id}"); + } + } + + [Fact] + public async Task GearResourceBoughtSort_OrdersByMostRecentlyAcquiredCopyDescending_IsOk() + { + await Authenticate(); + + var title = $"BoughtSort-{System.Guid.NewGuid():N}"; + var olderPurchase = await PostAsync($"/{ResourceEndpoint}", new GearDto + { + Title = title, + OwnedVersions = [new OwnedVersionDto { CopyType = CopyType.Physical, AcquiredAt = new DateOnly(2020, 1, 1) }] + }); + var recentPurchase = await PostAsync($"/{ResourceEndpoint}", new GearDto + { + Title = title, + OwnedVersions = [new OwnedVersionDto { CopyType = CopyType.Physical, AcquiredAt = new DateOnly(2024, 6, 1) }] + }); + var neverOwned = await PostAsync($"/{ResourceEndpoint}", new GearDto { Title = title }); + + try + { + var results = await GetAsync>($"/{ResourceEndpoint}?sort=bought&search={title}"); + var ids = results.Items.Select(x => x.Id).ToList(); + ids.IndexOf(recentPurchase.Id).Should().BeLessThan(ids.IndexOf(olderPurchase.Id), + "the most recently acquired copy sorts first"); + ids.Should().Contain(neverOwned.Id, "an unset acquisition date still falls back to the newest-first tie-break, it isn't excluded"); + } + finally + { + await DeleteAsync($"/{ResourceEndpoint}/{olderPurchase.Id}"); + await DeleteAsync($"/{ResourceEndpoint}/{recentPurchase.Id}"); + await DeleteAsync($"/{ResourceEndpoint}/{neverOwned.Id}"); + } + } + + [Fact] + public async Task GearResourceOwnedAndFavoriteFilters_OnlyReturnMatchingItems_IsOk() + { + await Authenticate(); + + var title = $"OwnedTarget-{System.Guid.NewGuid():N}"; + var owned = await PostAsync($"/{ResourceEndpoint}", new GearDto + { + Title = title, + // "owned" is derived from having at least one owned version, not a stored flag + OwnedVersions = [new OwnedVersionDto { CopyType = CopyType.Physical, Price = 199.99m, ProductName = "Limited edition" }] + }); + var favorite = await PostAsync($"/{ResourceEndpoint}", new GearDto { Title = title, IsFavorite = true }); + var plain = await PostAsync($"/{ResourceEndpoint}", new GearDto { Title = title }); + + try + { + var ownedResults = await GetAsync>($"/{ResourceEndpoint}?IsOwned=true&search={title}"); + ownedResults.Items.Should().ContainSingle(x => x.Id == owned.Id); + ownedResults.Items.Should().NotContain(x => x.Id == plain.Id); + // the version's fields must survive the full DTO -> model -> BSON round trip, including ProductName + ownedResults.Items.Single(x => x.Id == owned.Id).OwnedVersions.Should().BeEquivalentTo(owned.OwnedVersions); + + var favoriteResults = await GetAsync>($"/{ResourceEndpoint}?IsFavorite=true&search={title}"); + favoriteResults.Items.Should().ContainSingle(x => x.Id == favorite.Id); + favoriteResults.Items.Should().NotContain(x => x.Id == plain.Id); + } + finally + { + await DeleteAsync($"/{ResourceEndpoint}/{owned.Id}"); + await DeleteAsync($"/{ResourceEndpoint}/{favorite.Id}"); + await DeleteAsync($"/{ResourceEndpoint}/{plain.Id}"); + } + } +} diff --git a/test/WebApi.IntegrationTests/Resources/GenericVideoGameImportFixtureCsvBuilder.cs b/test/WebApi.IntegrationTests/Resources/GenericVideoGameImportFixtureCsvBuilder.cs new file mode 100644 index 00000000..de5d3e8f --- /dev/null +++ b/test/WebApi.IntegrationTests/Resources/GenericVideoGameImportFixtureCsvBuilder.cs @@ -0,0 +1,46 @@ +using System.Text; + +namespace Keeptrack.WebApi.IntegrationTests.Resources; + +/// +/// Builds a small, synthetic video game transaction-history CSV for tests - never use a real personal +/// export as a test fixture (same rule as ). +/// +internal static class GenericVideoGameImportFixtureCsvBuilder +{ + public const string GameTitle = "Keeptrack Video Game Import Test Game"; + public const string GamePlatform = "PS4"; + public const string GameProductName = "Keeptrack Video Game Import Test Game : Premium Edition"; + public const string GameVendor = "PlayStation Store"; + public const string GameTransactionId = "999888777001"; + public const string GameOrderId = "999888777002"; + + public const string SecondGameTitle = "Keeptrack Video Game Import Test Game Two"; + private const string SecondGameTransactionId = "999888777003"; + private const string SecondGameOrderId = "999888777004"; + + /// + /// Reproduces a real PSN export shape: a single order bundled three different DLC packs for the same + /// game under one shared Transaction Id/Order Id, distinguishable only by Product Name. + /// + public const string BundleTitle = "Keeptrack Video Game Import Test Bundle Game"; + private const string BundleTransactionId = "999888777005"; + private const string BundleOrderId = "999888777006"; + public const string BundleProductA = "Keeptrack Video Game Import Test Bundle Game - DLC A"; + public const string BundleProductB = "Keeptrack Video Game Import Test Bundle Game - DLC B"; + public const string BundleProductC = "Keeptrack Video Game Import Test Bundle Game - DLC C"; + + public static byte[] Build() + { + var csv = $""" + Transaction Date,Game Name,Product Name,Platform,Vendor,Transaction Id,Order Id,Final Price (€) + 2019-08-02,{GameTitle} ({GamePlatform}),{GameProductName},{GamePlatform},{GameVendor},{GameTransactionId},{GameOrderId},14.99 + 2021-03-15,{SecondGameTitle},{SecondGameTitle},PS5,{GameVendor},{SecondGameTransactionId},{SecondGameOrderId},59.99 + 2025-11-21,{BundleTitle},{BundleProductA},PS4,{GameVendor},{BundleTransactionId},{BundleOrderId},2.24 + 2025-11-21,{BundleTitle},{BundleProductB},PS4,{GameVendor},{BundleTransactionId},{BundleOrderId},1.12 + 2025-11-21,{BundleTitle},{BundleProductC},PS4,{GameVendor},{BundleTransactionId},{BundleOrderId},1.49 + + """; + return Encoding.UTF8.GetBytes(csv); + } +} diff --git a/test/WebApi.IntegrationTests/Resources/GenericVideoGameImportResourceTest.cs b/test/WebApi.IntegrationTests/Resources/GenericVideoGameImportResourceTest.cs new file mode 100644 index 00000000..088f67f8 --- /dev/null +++ b/test/WebApi.IntegrationTests/Resources/GenericVideoGameImportResourceTest.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using AwesomeAssertions; +using Keeptrack.Common.System; +using Keeptrack.WebApi.Contracts.Dto; +using Keeptrack.WebApi.IntegrationTests.Hosting; +using Xunit; + +namespace Keeptrack.WebApi.IntegrationTests.Resources; + +public class GenericVideoGameImportResourceTest(KestrelWebAppFactory factory) + : ResourceTestBase(factory) +{ + [Fact] + public async Task PreviewThenCommit_CreatesAVideoGame_AndSkipsADuplicateReimport() + { + await Authenticate(); + + var csv = GenericVideoGameImportFixtureCsvBuilder.Build(); + + var preview = await PostFileAsync>("/api/import/video-games/preview", "file", csv, "transactions.csv"); + var gameRow = preview.Should().Contain(r => r.Title == GenericVideoGameImportFixtureCsvBuilder.GameTitle).Subject; + gameRow.Platform.Should().Be(GenericVideoGameImportFixtureCsvBuilder.GamePlatform); + gameRow.ProductName.Should().Be(GenericVideoGameImportFixtureCsvBuilder.GameProductName); + gameRow.AlreadyImported.Should().BeFalse(); + + try + { + // a row with no platform must be rejected before anything is persisted + var invalidItem = ToCommitItem(gameRow); + invalidItem.Platform = null; + var invalidRequest = new GenericVideoGameImportCommitRequestDto { Items = [invalidItem] }; + await PostAsync("/api/import/video-games/commit", invalidRequest, HttpStatusCode.BadRequest); + + var commitRequest = new GenericVideoGameImportCommitRequestDto { Items = [ToCommitItem(gameRow)] }; + var commitResult = await PostAsync("/api/import/video-games/commit", commitRequest); + commitResult.VideoGamesCreated.Should().Be(1); + + var videoGames = await GetAsync>($"/api/video-games?search={Uri.EscapeDataString(GenericVideoGameImportFixtureCsvBuilder.GameTitle)}"); + var videoGame = videoGames.Items.Should().ContainSingle().Subject; + videoGame.Platforms.Should().ContainSingle(); + videoGame.Platforms[0].Platform.Should().Be(GenericVideoGameImportFixtureCsvBuilder.GamePlatform); + videoGame.Platforms[0].ProductName.Should().Be(GenericVideoGameImportFixtureCsvBuilder.GameProductName); + videoGame.Platforms[0].CopyType.Should().Be(CopyType.Digital); + videoGame.Platforms[0].Price.Should().Be(14.99m); + videoGame.Platforms[0].Reference.Should().Contain(GenericVideoGameImportFixtureCsvBuilder.GameTransactionId); + // SourceTitle is echoed from the preview row's already-cleaned Title (platform suffix already + // stripped by CleanTitle during parsing), not the raw "Game Name (PS4)" CSV cell. + videoGame.Notes.Should().Be($"Title from {GenericVideoGameImportFixtureCsvBuilder.GameVendor}: {GenericVideoGameImportFixtureCsvBuilder.GameTitle}"); + + // re-preview after commit: the just-imported transaction must now be flagged, so re-uploading a + // newer export later doesn't silently duplicate it + var secondPreview = await PostFileAsync>("/api/import/video-games/preview", "file", csv, "transactions.csv"); + secondPreview.Should().Contain(r => r.Title == GenericVideoGameImportFixtureCsvBuilder.GameTitle && r.AlreadyImported); + + // committing the exact same row again must not duplicate anything + var secondCommitResult = await PostAsync("/api/import/video-games/commit", commitRequest); + secondCommitResult.VideoGamesCreated.Should().Be(0); + secondCommitResult.VideoGamesMergedInto.Should().Be(0); + secondCommitResult.VideoGamesSkipped.Should().Be(1); + + var videoGamesAfterReimport = await GetAsync>($"/api/video-games?search={Uri.EscapeDataString(GenericVideoGameImportFixtureCsvBuilder.GameTitle)}"); + videoGamesAfterReimport.Items.Should().ContainSingle().Which.Platforms.Should().ContainSingle(); + } + finally + { + var videoGames = await GetAsync>($"/api/video-games?search={Uri.EscapeDataString(GenericVideoGameImportFixtureCsvBuilder.GameTitle)}"); + foreach (var videoGame in videoGames.Items.Where(g => g.Id is not null)) + { + await DeleteAsync($"/api/video-games/{videoGame.Id}"); + } + } + } + + [Fact] + public async Task Commit_MergesTwoRowsSharingATitle_IntoOneVideoGameWithTwoPlatforms() + { + await Authenticate(); + + const string sharedTitle = "Keeptrack Video Game Import Test Multi-Platform Game"; + + var request = new GenericVideoGameImportCommitRequestDto + { + Items = + [ + new GenericVideoGameImportCommitItemDto + { + RowId = "row-1", Title = sharedTitle, SourceTitle = sharedTitle, Platform = "PS4", + TransactionId = "111222333001", OrderId = "111222333002", Vendor = "PlayStation Store", CopyType = CopyType.Digital + }, + new GenericVideoGameImportCommitItemDto + { + RowId = "row-2", Title = sharedTitle, SourceTitle = sharedTitle, Platform = "PS5", + TransactionId = "111222333003", OrderId = "111222333004", Vendor = "PlayStation Store", CopyType = CopyType.Digital + } + ] + }; + + try + { + var commitResult = await PostAsync("/api/import/video-games/commit", request); + commitResult.VideoGamesCreated.Should().Be(1); + + var videoGames = await GetAsync>($"/api/video-games?search={Uri.EscapeDataString(sharedTitle)}"); + var videoGame = videoGames.Items.Should().ContainSingle().Subject; + videoGame.Platforms.Should().HaveCount(2); + videoGame.Platforms.Select(p => p.Platform).Should().BeEquivalentTo(["PS4", "PS5"]); + } + finally + { + var videoGames = await GetAsync>($"/api/video-games?search={Uri.EscapeDataString(sharedTitle)}"); + foreach (var videoGame in videoGames.Items.Where(g => g.Id is not null)) + { + await DeleteAsync($"/api/video-games/{videoGame.Id}"); + } + } + } + + [Fact] + public async Task PreviewThenCommit_ImportsAllThreeLines_WhenTheyShareOneTransactionAndOrderButDifferentProducts() + { + // reproduces a real bug found against a real PSN export: three different "Far Cry 4" DLC packs were + // bought in a single transaction, sharing one Transaction Id/Order Id - the Reference used to be + // built from those two alone, so the second and third lines were wrongly skipped as "already + // imported" duplicates of the first, and only 1 of the 3 platform entries actually got created + await Authenticate(); + + var csv = GenericVideoGameImportFixtureCsvBuilder.Build(); + + try + { + var preview = await PostFileAsync>("/api/import/video-games/preview", "file", csv, "transactions.csv"); + var bundleRows = preview.Where(r => r.Title == GenericVideoGameImportFixtureCsvBuilder.BundleTitle).ToList(); + bundleRows.Should().HaveCount(3); + bundleRows.Should().OnlyContain(r => !r.AlreadyImported); + + var commitRequest = new GenericVideoGameImportCommitRequestDto { Items = bundleRows.Select(ToCommitItem).ToList() }; + var commitResult = await PostAsync("/api/import/video-games/commit", commitRequest); + commitResult.VideoGamesCreated.Should().Be(1); + commitResult.VideoGamesSkipped.Should().Be(0); + + var videoGames = await GetAsync>($"/api/video-games?search={Uri.EscapeDataString(GenericVideoGameImportFixtureCsvBuilder.BundleTitle)}"); + var videoGame = videoGames.Items.Should().ContainSingle().Subject; + videoGame.Platforms.Should().HaveCount(3); + videoGame.Platforms.Select(p => p.ProductName).Should().BeEquivalentTo( + [ + GenericVideoGameImportFixtureCsvBuilder.BundleProductA, + GenericVideoGameImportFixtureCsvBuilder.BundleProductB, + GenericVideoGameImportFixtureCsvBuilder.BundleProductC + ]); + videoGame.Platforms.Select(p => p.Reference).Distinct().Should().HaveCount(3); + } + finally + { + var videoGames = await GetAsync>($"/api/video-games?search={Uri.EscapeDataString(GenericVideoGameImportFixtureCsvBuilder.BundleTitle)}"); + foreach (var videoGame in videoGames.Items.Where(g => g.Id is not null)) + { + await DeleteAsync($"/api/video-games/{videoGame.Id}"); + } + } + } + + private static GenericVideoGameImportCommitItemDto ToCommitItem(GenericVideoGameImportPreviewRowDto row) => new() + { + RowId = row.RowId, + Title = row.Title, + SourceTitle = row.Title, + Platform = row.Platform, + ProductName = row.ProductName, + TransactionId = row.TransactionId, + OrderId = row.OrderId, + Vendor = row.Vendor, + AcquiredAt = row.TransactionDate, + Price = row.Price, + CopyType = CopyType.Digital + }; +} diff --git a/test/WebApi.IntegrationTests/Resources/HealthProfileResourceTest.cs b/test/WebApi.IntegrationTests/Resources/HealthProfileResourceTest.cs index 78e8272f..d30cbd74 100644 --- a/test/WebApi.IntegrationTests/Resources/HealthProfileResourceTest.cs +++ b/test/WebApi.IntegrationTests/Resources/HealthProfileResourceTest.cs @@ -33,6 +33,7 @@ public async Task HealthProfileResourceFullCycle_IsOk() try { created.Notes = "Allergic to penicillin"; + created.ImageUrl = "https://example.com/profile.jpg"; await PutAsync($"/{ResourceEndpoint}/{created.Id}", created); var updated = await GetAsync($"/{ResourceEndpoint}/{created.Id}"); diff --git a/test/WebApi.IntegrationTests/Resources/HouseResourceTest.cs b/test/WebApi.IntegrationTests/Resources/HouseResourceTest.cs index 230a99ca..f8a931f7 100644 --- a/test/WebApi.IntegrationTests/Resources/HouseResourceTest.cs +++ b/test/WebApi.IntegrationTests/Resources/HouseResourceTest.cs @@ -33,6 +33,7 @@ public async Task HouseResourceFullCycle_IsOk() o.PropertyType = f.PickRandom(); o.MovedInAt = System.DateOnly.FromDateTime(f.Date.Past()); o.MovedOutAt = System.DateOnly.FromDateTime(f.Date.Recent()); + o.ImageUrl = f.Internet.Url(); }) .Generate(); var created = await PostAsync($"/{ResourceEndpoint}", input); diff --git a/test/WebApi.IntegrationTests/Resources/UnlinkReferenceResourceTest.cs b/test/WebApi.IntegrationTests/Resources/UnlinkReferenceResourceTest.cs new file mode 100644 index 00000000..7da16b4d --- /dev/null +++ b/test/WebApi.IntegrationTests/Resources/UnlinkReferenceResourceTest.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using AwesomeAssertions; +using Keeptrack.Common.System; +using Keeptrack.Domain.Models; +using Keeptrack.Domain.Repositories; +using Keeptrack.WebApi.Contracts.Dto; +using Keeptrack.WebApi.IntegrationTests.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Keeptrack.WebApi.IntegrationTests.Resources; + +/// +/// Exercises POST /api/{tv-shows,movies,books,video-games,albums}/{id}/unlink-reference - the admin +/// action that clears a tenant's own ReferenceId and permanently deletes the shared reference +/// document behind a wrong match. The integration suite's shared Firebase test user is an admin (see +/// FreeTierTest's own doc comment), so already +/// satisfies the endpoint's AdminOnly policy - the policy attribute itself is covered by a +/// reflection unit test instead (an HTTP 403 test would need a second, non-admin Firebase account this +/// suite doesn't have configured). +/// +public class UnlinkReferenceResourceTest(KestrelWebAppFactory factory) + : ResourceTestBase(factory) +{ + [Fact] + public async Task UnlinkReference_ClearsTvShowLink_AndDeletesTheReferenceDocument() + { + using var scope = Factory.Services.CreateScope(); + var referenceRepository = scope.ServiceProvider.GetRequiredService(); + var title = $"Unlink Reference Test Show {Guid.NewGuid()}"; + const int year = 2019; + + var reference = await referenceRepository.UpsertAsync(new TvShowReferenceModel + { + Title = "Canonical Title", + TitleNormalized = "canonical title", + Year = year, + ExternalIds = new Dictionary { ["tmdb"] = "1" }, + MatchedAliases = [new ReferenceMatchModel { Title = TitleNormalizer.Normalize(title), Year = year }] + }); + + await Authenticate(); + var created = await PostAsync("/api/tv-shows", new TvShowDto { Title = title, Year = year }); + await PostAsync($"/api/tv-shows/{created.Id}/refresh-reference", null, HttpStatusCode.OK); + + try + { + var unlinked = await PostAsync($"/api/tv-shows/{created.Id}/unlink-reference", null, HttpStatusCode.OK); + + unlinked!.ReferenceId.Should().BeNullOrEmpty(); + (await referenceRepository.FindByIdAsync(reference.Id!)).Should().BeNull(); + } + finally + { + await DeleteAsync($"/api/tv-shows/{created.Id}"); + } + } + + [Fact] + public async Task UnlinkReference_ClearsMovieLink_AndDeletesTheReferenceDocument() + { + using var scope = Factory.Services.CreateScope(); + var referenceRepository = scope.ServiceProvider.GetRequiredService(); + var title = $"Unlink Reference Test Movie {Guid.NewGuid()}"; + const int year = 2019; + + var reference = await referenceRepository.UpsertAsync(new MovieReferenceModel + { + Title = "Canonical Movie Title", + TitleNormalized = "canonical movie title", + Year = year, + ExternalIds = new Dictionary { ["tmdb"] = "1" }, + MatchedAliases = [new ReferenceMatchModel { Title = TitleNormalizer.Normalize(title), Year = year }] + }); + + await Authenticate(); + var created = await PostAsync("/api/movies", new MovieDto { Title = title, Year = year }); + await PostAsync($"/api/movies/{created.Id}/refresh-reference", null, HttpStatusCode.OK); + + try + { + var unlinked = await PostAsync($"/api/movies/{created.Id}/unlink-reference", null, HttpStatusCode.OK); + + unlinked!.ReferenceId.Should().BeNullOrEmpty(); + (await referenceRepository.FindByIdAsync(reference.Id!)).Should().BeNull(); + } + finally + { + await DeleteAsync($"/api/movies/{created.Id}"); + } + } + + [Fact] + public async Task UnlinkReference_ClearsBookLink_AndDeletesTheReferenceDocument() + { + using var scope = Factory.Services.CreateScope(); + var referenceRepository = scope.ServiceProvider.GetRequiredService(); + var title = $"Unlink Reference Test Book {Guid.NewGuid()}"; + const int year = 2019; + + var reference = await referenceRepository.UpsertAsync(new BookReferenceModel + { + Title = "Canonical Book Title", + TitleNormalized = "canonical book title", + Year = year, + ExternalIds = new Dictionary { ["openlibrary"] = "OL1W" }, + MatchedAliases = [new ReferenceMatchModel { Title = TitleNormalizer.Normalize(title), Year = year, Creator = TitleNormalizer.Normalize("Some Author") }] + }); + + await Authenticate(); + var created = await PostAsync("/api/books", new BookDto { Title = title, Author = "Some Author", Year = year }); + await PostAsync($"/api/books/{created.Id}/refresh-reference", null, HttpStatusCode.OK); + + try + { + var unlinked = await PostAsync($"/api/books/{created.Id}/unlink-reference", null, HttpStatusCode.OK); + + unlinked!.ReferenceId.Should().BeNullOrEmpty(); + (await referenceRepository.FindByIdAsync(reference.Id!)).Should().BeNull(); + } + finally + { + await DeleteAsync($"/api/books/{created.Id}"); + } + } + + [Fact] + public async Task UnlinkReference_ClearsVideoGameLink_AndDeletesTheReferenceDocument() + { + using var scope = Factory.Services.CreateScope(); + var referenceRepository = scope.ServiceProvider.GetRequiredService(); + var title = $"Unlink Reference Test Game {Guid.NewGuid()}"; + const int year = 2019; + + var reference = await referenceRepository.UpsertAsync(new VideoGameReferenceModel + { + Title = "Canonical Game Title", + TitleNormalized = "canonical game title", + Year = year, + ExternalIds = new Dictionary { ["rawg"] = "1" }, + MatchedAliases = [new ReferenceMatchModel { Title = TitleNormalizer.Normalize(title), Year = year }] + }); + + await Authenticate(); + var created = await PostAsync("/api/video-games", new VideoGameDto { Title = title, Year = year }); + await PostAsync($"/api/video-games/{created.Id}/refresh-reference", null, HttpStatusCode.OK); + + try + { + var unlinked = await PostAsync($"/api/video-games/{created.Id}/unlink-reference", null, HttpStatusCode.OK); + + unlinked!.ReferenceId.Should().BeNullOrEmpty(); + (await referenceRepository.FindByIdAsync(reference.Id!)).Should().BeNull(); + } + finally + { + await DeleteAsync($"/api/video-games/{created.Id}"); + } + } + + [Fact] + public async Task UnlinkReference_ClearsAlbumLink_AndDeletesTheReferenceDocument() + { + using var scope = Factory.Services.CreateScope(); + var referenceRepository = scope.ServiceProvider.GetRequiredService(); + var title = $"Unlink Reference Test Album {Guid.NewGuid()}"; + const int year = 2019; + + var reference = await referenceRepository.UpsertAsync(new AlbumReferenceModel + { + Title = "Canonical Album Title", + TitleNormalized = "canonical album title", + Year = year, + ExternalIds = new Dictionary { ["discogs"] = "1" }, + MatchedAliases = [new ReferenceMatchModel { Title = TitleNormalizer.Normalize(title), Year = year, Creator = TitleNormalizer.Normalize("Some Artist") }] + }); + + await Authenticate(); + var created = await PostAsync("/api/albums", new AlbumDto { Title = title, Artist = "Some Artist", Year = year }); + await PostAsync($"/api/albums/{created.Id}/refresh-reference", null, HttpStatusCode.OK); + + try + { + var unlinked = await PostAsync($"/api/albums/{created.Id}/unlink-reference", null, HttpStatusCode.OK); + + unlinked!.ReferenceId.Should().BeNullOrEmpty(); + (await referenceRepository.FindByIdAsync(reference.Id!)).Should().BeNull(); + } + finally + { + await DeleteAsync($"/api/albums/{created.Id}"); + } + } +} diff --git a/test/WebApi.IntegrationTests/Resources/UserPreferencesResourceTest.cs b/test/WebApi.IntegrationTests/Resources/UserPreferencesResourceTest.cs new file mode 100644 index 00000000..414b602e --- /dev/null +++ b/test/WebApi.IntegrationTests/Resources/UserPreferencesResourceTest.cs @@ -0,0 +1,50 @@ +using System.Net; +using System.Threading.Tasks; +using AwesomeAssertions; +using Keeptrack.WebApi.Contracts.Dto; +using Keeptrack.WebApi.IntegrationTests.Hosting; +using Xunit; + +namespace Keeptrack.WebApi.IntegrationTests.Resources; + +/// +/// Covers the singleton-per-owner preferences resource: the "nothing saved yet" default, and the +/// upsert-by-owner-id persistence the repository relies on (a first PUT inserts, a later one updates the +/// same document rather than colliding with the unique owner_id index). +/// +public class UserPreferencesResourceTest(KestrelWebAppFactory factory) + : ResourceTestBase(factory) +{ + private const string ResourceEndpoint = "api/user-preferences"; + + [Fact] + public async Task UserPreferences_RequireAuthentication() + { + await GetAsync($"/{ResourceEndpoint}", HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Get_ReturnsAllFalseDefaults_WhenNothingWasEverSaved() + { + await Authenticate(); + + var preferences = await GetAsync($"/{ResourceEndpoint}"); + + preferences.Features.ShowChasseAuxLivresLink.Should().BeFalse(); + } + + [Fact] + public async Task Put_ThenGet_RoundTripsTheSavedValue() + { + await Authenticate(); + + await PutAsync($"/{ResourceEndpoint}", new UserPreferencesDto { Features = new UserPreferencesFeaturesDto { ShowChasseAuxLivresLink = true } }); + var afterFirstSave = await GetAsync($"/{ResourceEndpoint}"); + afterFirstSave.Features.ShowChasseAuxLivresLink.Should().BeTrue(); + + // a second PUT must update the same document (upsert by owner_id), not collide with the unique index + await PutAsync($"/{ResourceEndpoint}", new UserPreferencesDto { Features = new UserPreferencesFeaturesDto { ShowChasseAuxLivresLink = false } }); + var afterSecondSave = await GetAsync($"/{ResourceEndpoint}"); + afterSecondSave.Features.ShowChasseAuxLivresLink.Should().BeFalse(); + } +} diff --git a/test/WebApi.IntegrationTests/Resources/VideoGameResourceTest.cs b/test/WebApi.IntegrationTests/Resources/VideoGameResourceTest.cs index 6d0b2af3..2c02a780 100644 --- a/test/WebApi.IntegrationTests/Resources/VideoGameResourceTest.cs +++ b/test/WebApi.IntegrationTests/Resources/VideoGameResourceTest.cs @@ -1,12 +1,17 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; using AwesomeAssertions; using Bogus; using Keeptrack.Common.System; +using Keeptrack.Domain.Repositories; +using Keeptrack.Infrastructure.MongoDb.Entities; using Keeptrack.WebApi.Contracts.Dto; using Keeptrack.WebApi.IntegrationTests.Hosting; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; using Xunit; namespace Keeptrack.WebApi.IntegrationTests.Resources; @@ -32,6 +37,10 @@ public async Task VideoGameResourceFullCycle_IsOk() { o.Title = f.Random.AlphaNumeric(14); o.Platforms = [new VideoGamePlatformDto { Platform = "PC", CopyType = CopyType.Physical, State = "Current" }]; + // round-trips CustomImageUrl through the real Mapperly mappers + MongoDB - a plain scalar field + // with no special mapping, but a real integration test is what would catch a missed + // [BsonElement]/mapper ignore, not a mocked unit test. + o.CustomImageUrl = f.Internet.Url(); }) .Generate(); var created = await PostAsync($"/{ResourceEndpoint}", input); @@ -122,4 +131,47 @@ public async Task VideoGameResourceOwnedAndWishlistedFilters_OnlyReturnMatchingI await DeleteAsync($"/{ResourceEndpoint}/{created.Id}"); } } + + /// + /// follows 's exact shape - + /// VideoGameController.OnListMappedAsync is expected to apply it over the linked reference's own + /// cover on every list read. + /// + [Fact] + public async Task VideoGameResourceList_CustomImageUrlOverridesTheLinkedReferencesCover() + { + using var scope = Factory.Services.CreateScope(); + var referenceRepository = scope.ServiceProvider.GetRequiredService(); + var uniqueTitle = $"CustomImageOverrideTarget-{Guid.NewGuid():N}"; + + var reference = await referenceRepository.UpsertAsync(new Keeptrack.Domain.Models.VideoGameReferenceModel + { + Title = "Some Reference Title", + TitleNormalized = "some reference title", + ExternalIds = new Dictionary { ["rawg"] = $"rawg-{Guid.NewGuid():N}" }, + ImageUrl = "https://example.com/reference-cover.jpg" + }); + + await Authenticate(); + const string customImageUrl = "https://example.com/custom-cover.jpg"; + var created = await PostAsync($"/{ResourceEndpoint}", new VideoGameDto + { + Title = uniqueTitle, + ReferenceId = reference.Id, + CustomImageUrl = customImageUrl + }); + + try + { + var list = await GetAsync>($"/{ResourceEndpoint}?search={uniqueTitle}"); + var item = list.Items.Should().ContainSingle(x => x.Id == created.Id).Subject; + item.ImageUrl.Should().Be(customImageUrl); + } + finally + { + await DeleteAsync($"/{ResourceEndpoint}/{created.Id}"); + var referenceCollection = scope.ServiceProvider.GetRequiredService().GetCollection("videogame_reference"); + await referenceCollection.DeleteOneAsync(Builders.Filter.Eq(x => x.Id, reference.Id), TestContext.Current.CancellationToken); + } + } } diff --git a/test/WebApi.UnitTests/Controllers/FreeTierTest.cs b/test/WebApi.UnitTests/Controllers/FreeTierTest.cs index e9750def..0434f941 100644 --- a/test/WebApi.UnitTests/Controllers/FreeTierTest.cs +++ b/test/WebApi.UnitTests/Controllers/FreeTierTest.cs @@ -155,6 +155,8 @@ public async Task Post_NeverCapsAnAdmin() [InlineData(typeof(HouseHistoryController), "MemberOnly")] [InlineData(typeof(HealthProfileController), "MemberOnly")] [InlineData(typeof(HealthRecordController), "MemberOnly")] + [InlineData(typeof(CollectibleController), "MemberOnly")] + [InlineData(typeof(GearController), "MemberOnly")] [InlineData(typeof(TvTimeImportController), "MemberOnly")] [InlineData(typeof(CarHistoryImportController), "MemberOnly")] [InlineData(typeof(HealthImportController), "MemberOnly")] diff --git a/test/WebApi.UnitTests/Controllers/UnlinkReferenceAuthorizationTest.cs b/test/WebApi.UnitTests/Controllers/UnlinkReferenceAuthorizationTest.cs new file mode 100644 index 00000000..8df92675 --- /dev/null +++ b/test/WebApi.UnitTests/Controllers/UnlinkReferenceAuthorizationTest.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using System.Reflection; +using AwesomeAssertions; +using Keeptrack.WebApi.Controllers; +using Microsoft.AspNetCore.Authorization; +using Xunit; + +namespace Keeptrack.WebApi.UnitTests.Controllers; + +/// +/// The "unlink-reference" action on each reference-linked controller deletes shared data (the reference +/// document itself), unlike the harmless/idempotent "refresh-reference" action next to it - it must +/// carry its own method-level AdminOnly policy on top of whatever the controller class itself +/// requires. A reflection guard here (same shape as FreeTierTest.Controller_CarriesTheExpectedAuthorizationPolicy, +/// just at the method level instead of the class level) means removing the attribute is a failing test, +/// not a silent privilege-escalation bug - the integration suite can't exercise a 403 here directly since +/// its one Firebase test user is an admin (see FreeTierTest's own doc comment). +/// +[Trait("Category", "UnitTests")] +public class UnlinkReferenceAuthorizationTest +{ + [Theory] + [InlineData(typeof(TvShowController))] + [InlineData(typeof(MovieController))] + [InlineData(typeof(BookController))] + [InlineData(typeof(VideoGameController))] + [InlineData(typeof(AlbumController))] + public void UnlinkReference_CarriesTheAdminOnlyPolicy(Type controllerType) + { + var method = controllerType.GetMethod("UnlinkReference", BindingFlags.Public | BindingFlags.Instance) + ?? throw new InvalidOperationException($"{controllerType.Name} has no UnlinkReference method."); + + var attribute = method.GetCustomAttributes(inherit: false).Single(); + + attribute.Policy.Should().Be("AdminOnly"); + } +} diff --git a/test/WebApi.UnitTests/Services/AmazonImportMergeServiceTest.cs b/test/WebApi.UnitTests/Services/AmazonImportMergeServiceTest.cs new file mode 100644 index 00000000..e47f47a3 --- /dev/null +++ b/test/WebApi.UnitTests/Services/AmazonImportMergeServiceTest.cs @@ -0,0 +1,32 @@ +using AwesomeAssertions; +using Keeptrack.Domain.Services; +using Xunit; + +namespace Keeptrack.WebApi.UnitTests.Services; + +[Trait("Category", "UnitTests")] +public class AmazonImportMergeServiceTest +{ + private const string SomeOrderId = "405-1111111-1111111"; + private const string SomeAsin = "0552177571"; + + [Fact] + public void FormatOrderReference_IncludesBothTheOrderIdAndTheAsin() + { + AmazonImportMergeService.FormatOrderReference(SomeOrderId, SomeAsin).Should().Be($"Amazon order {SomeOrderId} (ASIN {SomeAsin})"); + } + + [Fact] + public void BuildAmazonProvenanceNotes_IncludesTheIsbnLine_WhenAnIsbnIsGiven() + { + AmazonImportMergeService.BuildAmazonProvenanceNotes("The Secret: Jack Reacher, Book 28", "0552177571") + .Should().Be("Title from Amazon: The Secret: Jack Reacher, Book 28\nISBN from Amazon: 0552177571"); + } + + [Fact] + public void BuildAmazonProvenanceNotes_OmitsTheIsbnLine_WhenThereIsNoIsbn() + { + AmazonImportMergeService.BuildAmazonProvenanceNotes("A Book With No Isbn", null) + .Should().Be("Title from Amazon: A Book With No Isbn"); + } +} diff --git a/test/WebApi.UnitTests/Services/AmazonOrderPreviewServiceTest.cs b/test/WebApi.UnitTests/Services/AmazonOrderPreviewServiceTest.cs new file mode 100644 index 00000000..47438ae5 --- /dev/null +++ b/test/WebApi.UnitTests/Services/AmazonOrderPreviewServiceTest.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using AwesomeAssertions; +using Keeptrack.Domain.Services; +using Xunit; + +namespace Keeptrack.WebApi.UnitTests.Services; + +[Trait("Category", "UnitTests")] +public class AmazonOrderPreviewServiceTest +{ + private static Stream ToStream(string csv) => new MemoryStream(Encoding.UTF8.GetBytes(csv)); + + // The "é" sequence below is deliberately literal (not a copy/paste accident) - it reproduces the real + // export's mojibake byte-for-byte, confirmed against the raw bytes of a real Amazon order-history CSV. + private const string Csv = """ + ASIN,Order Date,Order ID,Product Name,Product Condition,Total Amount,Website + 0552177571,2024-01-24T09:01:58Z,405-2296545-4493925,"protection d'écrans",New,10.49,Amazon.fr + B002KMW6ZI,2010-12-06T09:02:46Z,402-4436663-3305145,Some Gadget,Neuf,'19.16',Amazon.fr + """; + + [Fact] + public void BuildPreview_RepairsMojibakeInTheProductName() + { + var result = AmazonOrderPreviewService.BuildPreview(ToStream(Csv), new HashSet()); + + result[0].Title.Should().Be("protection d'écrans"); + } + + [Fact] + public void BuildPreview_DecodesARawNumericHtmlEntityInTheProductName() + { + const string csv = """ + ASIN,Order Date,Order ID,Product Name,Product Condition,Total Amount,Website + 2811205063,2020-02-02T09:26:31Z,403-0337751-9465130,"Sorceleur, Tome 1: Le Dernier Vœu",New,7.1,Amazon.fr + """; + + var result = AmazonOrderPreviewService.BuildPreview(ToStream(csv), new HashSet()); + + result[0].Title.Should().Be("Sorceleur, Tome 1: Le Dernier Vœu"); + } + + [Fact] + public void BuildPreview_FlagsAnIsbn10ShapedAsinAsLikelyABook() + { + var result = AmazonOrderPreviewService.BuildPreview(ToStream(Csv), new HashSet()); + + result[0].LooksLikeBook.Should().BeTrue(); + result[0].SuggestedIsbn.Should().Be("0552177571"); + } + + [Fact] + public void BuildPreview_DoesNotFlagANonIsbnAsin() + { + var result = AmazonOrderPreviewService.BuildPreview(ToStream(Csv), new HashSet()); + + result[1].LooksLikeBook.Should().BeFalse(); + result[1].SuggestedIsbn.Should().BeNull(); + } + + [Fact] + public void BuildPreview_AcceptsAnXCheckDigitIsbn10() + { + const string csv = """ + ASIN,Order Date,Order ID,Product Name,Product Condition,Total Amount,Website + 080442957X,2020-01-01T00:00:00Z,111-1111111-1111111,A Book,New,9.99,Amazon.fr + """; + + var result = AmazonOrderPreviewService.BuildPreview(ToStream(csv), new HashSet()); + + result[0].LooksLikeBook.Should().BeTrue(); + } + + [Fact] + public void BuildPreview_StripsAmazonsExcelFormulaInjectionApostropheBeforeParsingThePrice() + { + var result = AmazonOrderPreviewService.BuildPreview(ToStream(Csv), new HashSet()); + + result[1].Price.Should().Be(19.16m); + } + + [Fact] + public void BuildPreview_FlagsARowWhoseOrderIdAndAsinAreAlreadyImported() + { + var alreadyImported = new HashSet { AmazonImportMergeService.FormatOrderReference("405-2296545-4493925", "0552177571") }; + + var result = AmazonOrderPreviewService.BuildPreview(ToStream(Csv), alreadyImported); + + result[0].AlreadyImported.Should().BeTrue(); + result[1].AlreadyImported.Should().BeFalse(); + } + + [Fact] + public void BuildPreview_DoesNotFlagADifferentItemFromTheSameOrder() + { + // reproduces a real bug: an order commonly contains several different line items, so matching by + // order id alone incorrectly flagged every sibling item once any one of them had been imported + const string csv = """ + ASIN,Order Date,Order ID,Product Name,Product Condition,Total Amount,Website + 0552177571,2024-01-24T09:01:58Z,405-2296545-4493925,"The Secret",New,10.49,Amazon.fr + B002KMW6ZI,2024-01-24T09:01:58Z,405-2296545-4493925,"Some Gadget",New,14.99,Amazon.fr + """; + // only the first item (ASIN 0552177571) of this order was actually imported before + var alreadyImported = new HashSet { AmazonImportMergeService.FormatOrderReference("405-2296545-4493925", "0552177571") }; + + var result = AmazonOrderPreviewService.BuildPreview(ToStream(csv), alreadyImported); + + result[0].AlreadyImported.Should().BeTrue(); + result[1].AlreadyImported.Should().BeFalse(); + } +} diff --git a/test/WebApi.UnitTests/Services/AmazonReferenceTest.cs b/test/WebApi.UnitTests/Services/AmazonReferenceTest.cs new file mode 100644 index 00000000..71844af7 --- /dev/null +++ b/test/WebApi.UnitTests/Services/AmazonReferenceTest.cs @@ -0,0 +1,49 @@ +using AwesomeAssertions; +using Keeptrack.Common.System; +using Keeptrack.Domain.Services; +using Xunit; + +namespace Keeptrack.WebApi.UnitTests.Services; + +/// +/// is the read side of 's +/// write side - covers both the round trip between them and the parts unique to the read side (vendor domain). +/// +[Trait("Category", "UnitTests")] +public class AmazonReferenceTest +{ + [Fact] + public void TryExtractAsin_RoundTrips_WhatFormatOrderReferenceWrote() + { + var reference = AmazonImportMergeService.FormatOrderReference("405-1111111-1111111", "0552177571"); + + AmazonReference.TryExtractAsin(reference).Should().Be("0552177571"); + } + + [Fact] + public void TryExtractAsin_ReturnsNull_ForAManuallyEnteredReferenceWithNoAsin() + { + AmazonReference.TryExtractAsin("Special edition, box 3").Should().BeNull(); + } + + [Fact] + public void TryExtractAsin_ReturnsNull_ForNullOrEmptyReference() + { + AmazonReference.TryExtractAsin(null).Should().BeNull(); + AmazonReference.TryExtractAsin("").Should().BeNull(); + } + + [Fact] + public void BuildProductUrl_UsesTheVendorsOwnAmazonDomain_WhenItLooksLikeOne() + { + AmazonReference.BuildProductUrl("0552177571", "Amazon.fr").Should().Be("https://amazon.fr/dp/0552177571"); + AmazonReference.BuildProductUrl("0552177571", "www.amazon.de").Should().Be("https://www.amazon.de/dp/0552177571"); + } + + [Fact] + public void BuildProductUrl_FallsBackToAmazonFr_WhenTheVendorIsntRecognizablyAmazon() + { + AmazonReference.BuildProductUrl("0552177571", "Fnac").Should().Be("https://www.amazon.fr/dp/0552177571"); + AmazonReference.BuildProductUrl("0552177571", null).Should().Be("https://www.amazon.fr/dp/0552177571"); + } +} diff --git a/test/WebApi.UnitTests/Services/GenericVideoGameImportServiceTest.cs b/test/WebApi.UnitTests/Services/GenericVideoGameImportServiceTest.cs new file mode 100644 index 00000000..a5645956 --- /dev/null +++ b/test/WebApi.UnitTests/Services/GenericVideoGameImportServiceTest.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using AwesomeAssertions; +using Keeptrack.Domain.Services; +using Xunit; + +namespace Keeptrack.WebApi.UnitTests.Services; + +[Trait("Category", "UnitTests")] +public class GenericVideoGameImportServiceTest +{ + private static Stream ToStream(string csv) => new MemoryStream(Encoding.UTF8.GetBytes(csv)); + + private const string Csv = """ + Transaction Date,Game Name,Product Name,Platform,Vendor,Transaction Id,Order Id,Final Price (€) + 2019-08-02,Grand Theft Auto V (PS4),Grand Theft Auto V : Édition Premium,PS4,PlayStation Store,148888333444,70557088055,14.99 + 2021-03-15,Returnal,Returnal,PS5,PlayStation Store,200000000001,80000000001,79.99 + """; + + [Fact] + public void BuildPreview_StripsTheTrailingPlatformSuffixFromTheTitle() + { + var result = GenericVideoGameImportService.BuildPreview(ToStream(Csv), new HashSet()); + + result[0].Title.Should().Be("Grand Theft Auto V"); + } + + [Fact] + public void BuildPreview_LeavesATitleWithNoMatchingPlatformSuffixUnchanged() + { + var result = GenericVideoGameImportService.BuildPreview(ToStream(Csv), new HashSet()); + + result[1].Title.Should().Be("Returnal"); + } + + [Fact] + public void BuildPreview_CarriesTheProductNamePlatformAndVendorThrough() + { + var result = GenericVideoGameImportService.BuildPreview(ToStream(Csv), new HashSet()); + + result[0].ProductName.Should().Be("Grand Theft Auto V : Édition Premium"); + result[0].Platform.Should().Be("PS4"); + result[0].Vendor.Should().Be("PlayStation Store"); + } + + [Fact] + public void BuildPreview_ParsesThePriceAndTransactionDate() + { + var result = GenericVideoGameImportService.BuildPreview(ToStream(Csv), new HashSet()); + + result[0].Price.Should().Be(14.99m); + result[0].TransactionDate.Should().Be(new System.DateOnly(2019, 8, 2)); + } + + [Fact] + public void BuildPreview_LeavesPriceNull_WhenTheColumnIsBlank() + { + const string csv = """ + Transaction Date,Game Name,Product Name,Platform,Vendor,Transaction Id,Order Id,Final Price (€) + 2019-08-02,Some Game,Some Game,PS4,PlayStation Store,148888333444,70557088055, + """; + + var result = GenericVideoGameImportService.BuildPreview(ToStream(csv), new HashSet()); + + result[0].Price.Should().BeNull(); + } + + [Fact] + public void BuildPreview_FlagsARowWhoseTransactionOrderAndProductAreAlreadyImported() + { + var alreadyImported = new HashSet + { + GenericVideoGameImportService.FormatReference("148888333444", "70557088055", "Grand Theft Auto V : Édition Premium", "Grand Theft Auto V (PS4)") + }; + + var result = GenericVideoGameImportService.BuildPreview(ToStream(Csv), alreadyImported); + + result[0].AlreadyImported.Should().BeTrue(); + result[1].AlreadyImported.Should().BeFalse(); + } + + [Fact] + public void FormatReference_IncludesTheOrderIdTransactionIdAndProductName() + { + GenericVideoGameImportService.FormatReference("148888333444", "70557088055", "Grand Theft Auto V : Édition Premium", "Grand Theft Auto V (PS4)") + .Should().Be("Order 70557088055 (transaction 148888333444) - Grand Theft Auto V : Édition Premium"); + } + + [Fact] + public void FormatReference_FallsBackToTheTitle_WhenProductNameIsBlank() + { + GenericVideoGameImportService.FormatReference("148888333444", "70557088055", null, "Grand Theft Auto V (PS4)") + .Should().Be("Order 70557088055 (transaction 148888333444) - Grand Theft Auto V (PS4)"); + } + + [Fact] + public void FormatReference_Disambiguates_WhenTwoLinesShareTheSameTransactionAndOrder() + { + // reproduces a real bug: a single PSN transaction can bundle several different DLC packs under the + // exact same Transaction Id/Order Id, distinguishable only by Product Name + var first = GenericVideoGameImportService.FormatReference("786966888755333", "399229990555", "FAR CRY 4 Vallée des Yétis", "FAR CRY 4"); + var second = GenericVideoGameImportService.FormatReference("786966888755333", "399229990555", "Pack Hurk Deluxe", "FAR CRY 4"); + + first.Should().NotBe(second); + } + + [Fact] + public void BuildProvenanceNotes_IncludesTheVendorAndSourceTitle() + { + GenericVideoGameImportService.BuildProvenanceNotes("PlayStation Store", "Grand Theft Auto V (PS4)") + .Should().Be("Title from PlayStation Store: Grand Theft Auto V (PS4)"); + } + + [Fact] + public void CleanTitle_IsCaseInsensitiveOnThePlatformSuffix() + { + GenericVideoGameImportService.CleanTitle("Returnal (ps5)", "PS5").Should().Be("Returnal"); + } +} diff --git a/test/WebApi.UnitTests/Services/OwnedItemImportMergeServiceTest.cs b/test/WebApi.UnitTests/Services/OwnedItemImportMergeServiceTest.cs new file mode 100644 index 00000000..bc8a94ca --- /dev/null +++ b/test/WebApi.UnitTests/Services/OwnedItemImportMergeServiceTest.cs @@ -0,0 +1,232 @@ +using System.Collections.Generic; +using System.Linq; +using AwesomeAssertions; +using Keeptrack.Domain.Models; +using Keeptrack.Domain.Services; +using Xunit; + +namespace Keeptrack.WebApi.UnitTests.Services; + +[Trait("Category", "UnitTests")] +public class OwnedItemImportMergeServiceTest +{ + private const string OwnerId = "owner-1"; + private const string SomeOrderId = "405-1111111-1111111"; + private const string SomeAsin = "0552177571"; + + private static BookModel Book(string title, params OwnedVersionModel[] ownedVersions) => + new() { Id = title, OwnerId = OwnerId, Title = title, Author = string.Empty, OwnedVersions = [.. ownedVersions] }; + + private static VideoGameModel Game(string title, params VideoGamePlatformModel[] platforms) => + new() { Id = title, OwnerId = OwnerId, Title = title, Platforms = [.. platforms] }; + + private static AmazonOwnedItemImportRequestItem Item(string title, string? reference = null, string? amazonTitle = null, string? isbn = null) => new() + { + Title = title, + AmazonTitle = amazonTitle ?? title, + Isbn = isbn, + OwnedVersion = new OwnedVersionModel { Reference = reference } + }; + + /// Wires up the generic engine for BookModel, the same way AmazonImportController does. + private static ImportCommitPlan ComputeBookPlan(IReadOnlyCollection existing, IReadOnlyList items) => + OwnedItemImportMergeService.ComputeCommitPlan( + existing, items, + b => b.Title, b => b.OwnedVersions.Select(v => v.Reference), + i => i.Title, i => i.OwnedVersion.Reference, + item => new BookModel + { + OwnerId = OwnerId, + Title = item.Title, + Author = string.Empty, + Notes = AmazonImportMergeService.BuildAmazonProvenanceNotes(item.AmazonTitle, item.Isbn), + OwnedVersions = [item.OwnedVersion] + }, + (book, item) => book.OwnedVersions.Add(item.OwnedVersion)); + + [Fact] + public void ComputeCommitPlan_CreatesANewBook_WhenNoExistingBookMatchesTheTitle() + { + var plan = ComputeBookPlan([], [Item("The Secret")]); + + plan.ItemsToCreate.Should().ContainSingle(); + plan.ItemsToCreate[0].Title.Should().Be("The Secret"); + plan.ItemsToCreate[0].Author.Should().Be(string.Empty); + plan.ItemsToCreate[0].OwnedVersions.Should().ContainSingle(); + plan.ItemsToUpdate.Should().BeEmpty(); + plan.OwnedCopiesAdded.Should().Be(1); + } + + [Fact] + public void ComputeCommitPlan_RecordsAmazonsOriginalTitleAndIsbnInNotes_ForANewlyCreatedBook() + { + var item = Item("The Secret", amazonTitle: "The Secret: Jack Reacher, Book 28", isbn: "0552177571"); + + var plan = ComputeBookPlan([], [item]); + + plan.ItemsToCreate[0].Notes.Should().Be("Title from Amazon: The Secret: Jack Reacher, Book 28\nISBN from Amazon: 0552177571"); + } + + [Fact] + public void ComputeCommitPlan_OmitsTheIsbnNoteLine_WhenThereIsNoIsbn() + { + var item = Item("A Book With No Isbn", amazonTitle: "A Book With No Isbn"); + + var plan = ComputeBookPlan([], [item]); + + plan.ItemsToCreate[0].Notes.Should().Be("Title from Amazon: A Book With No Isbn"); + } + + [Fact] + public void ComputeCommitPlan_DoesNotTouchNotes_WhenMergingIntoAnExistingBook() + { + var existing = Book("Some Book"); + existing.Notes = "My own pre-existing notes"; + + var plan = ComputeBookPlan([existing], [Item("some book", amazonTitle: "Some Book (Amazon listing)")]); + + plan.ItemsToCreate.Should().BeEmpty(); + existing.Notes.Should().Be("My own pre-existing notes"); + } + + [Fact] + public void ComputeCommitPlan_MergesIntoAnExistingBook_WhenTheNormalizedTitleMatches() + { + var existing = Book(" Some Book "); + + var plan = ComputeBookPlan([existing], [Item("some book")]); + + plan.ItemsToCreate.Should().BeEmpty(); + plan.ItemsToUpdate.Should().ContainSingle().Which.Should().BeSameAs(existing); + existing.OwnedVersions.Should().ContainSingle(); + plan.OwnedCopiesAdded.Should().Be(1); + } + + [Fact] + public void ComputeCommitPlan_MergesTwoSelectedRowsSharingATitle_IntoOneNewBook() + { + var plan = ComputeBookPlan([], [Item("Duplicate Title"), Item("duplicate title")]); + + plan.ItemsToCreate.Should().ContainSingle(); + plan.ItemsToCreate[0].OwnedVersions.Should().HaveCount(2); + plan.ItemsToUpdate.Should().BeEmpty(); + plan.OwnedCopiesAdded.Should().Be(2); + } + + [Fact] + public void ComputeCommitPlan_SkipsARow_WhenItsOrderReferenceAlreadyExistsOnAnExistingBook() + { + // reproduces a real bug: re-running the same commit (or re-importing the same export) created a + // second owned version for the same order every time, because matching was title-only + var reference = AmazonImportMergeService.FormatOrderReference(SomeOrderId, SomeAsin); + var existing = Book("Some Book", new OwnedVersionModel { Reference = reference }); + + var plan = ComputeBookPlan([existing], [Item("Some Book", reference: reference)]); + + plan.ItemsToCreate.Should().BeEmpty(); + plan.ItemsToUpdate.Should().BeEmpty(); + existing.OwnedVersions.Should().ContainSingle(); + plan.OwnedCopiesAdded.Should().Be(0); + plan.OwnedCopiesSkipped.Should().Be(1); + } + + [Fact] + public void ComputeCommitPlan_SkipsARow_WhenItsOrderReferenceMatchesAnExistingBook_EvenUnderADifferentTitleNow() + { + // reproduces a real bug: after a book's title was changed (e.g. by reference-data linking) following + // a first import, re-importing the same order under its original Amazon title no longer matched the + // existing book by title at all, and created a brand new duplicate record instead + var reference = AmazonImportMergeService.FormatOrderReference(SomeOrderId, SomeAsin); + var existing = Book("The Secret: Jack Reacher, Book 28", new OwnedVersionModel { Reference = reference }); + existing.Title = "No Plan B"; // renamed after the first import, e.g. by reference-data linking + + var plan = ComputeBookPlan([existing], [Item("The Secret: Jack Reacher, Book 28", reference: reference)]); + + plan.ItemsToCreate.Should().BeEmpty(); + plan.ItemsToUpdate.Should().BeEmpty(); + existing.OwnedVersions.Should().ContainSingle(); + plan.OwnedCopiesSkipped.Should().Be(1); + } + + [Fact] + public void ComputeCommitPlan_StillMergesASecondCopy_WhenTheOrderReferenceIsGenuinelyDifferent() + { + var existing = Book("Some Book", new OwnedVersionModel { Reference = AmazonImportMergeService.FormatOrderReference(SomeOrderId, SomeAsin) }); + + var plan = ComputeBookPlan([existing], [Item("some book", reference: AmazonImportMergeService.FormatOrderReference("405-9999999-9999999", SomeAsin))]); + + plan.ItemsToUpdate.Should().ContainSingle().Which.Should().BeSameAs(existing); + existing.OwnedVersions.Should().HaveCount(2); + plan.OwnedCopiesSkipped.Should().Be(0); + } + + [Fact] + public void ComputeCommitPlan_ImportsBothItems_WhenTwoDifferentItemsShareTheSameOrderId() + { + // reproduces a real bug: an Amazon order commonly contains several different line items, but the + // reference used to be built from the order id alone - so the second item in the same order was + // silently skipped as a "duplicate" of the first, even though it's a genuinely different product + const string sharedOrderId = "405-2222222-2222222"; + var firstItem = Item("First Book In Order", reference: AmazonImportMergeService.FormatOrderReference(sharedOrderId, "1111111111")); + var secondItem = Item("Second Book In Order", reference: AmazonImportMergeService.FormatOrderReference(sharedOrderId, "2222222222")); + + var plan = ComputeBookPlan([], [firstItem, secondItem]); + + plan.ItemsToCreate.Should().HaveCount(2); + plan.ItemsToCreate.Select(b => b.Title).Should().BeEquivalentTo(["First Book In Order", "Second Book In Order"]); + plan.OwnedCopiesAdded.Should().Be(2); + plan.OwnedCopiesSkipped.Should().Be(0); + } + + [Fact] + public void ComputeCommitPlan_WorksUnmodifiedForVideoGames_ViaPlatformsInsteadOfOwnedVersions() + { + var item = new AmazonVideoGameImportRequestItem + { + Title = "L.A. Noire", + AmazonTitle = "L.A. Noire", + Platform = new VideoGamePlatformModel { Platform = "PS3" } + }; + + var plan = OwnedItemImportMergeService.ComputeCommitPlan( + new List(), [item], + g => g.Title, g => g.Platforms.Select(p => p.Reference), + i => i.Title, i => i.Platform.Reference, + requestItem => new VideoGameModel { OwnerId = OwnerId, Title = requestItem.Title, Platforms = [requestItem.Platform] }, + (game, requestItem) => game.Platforms.Add(requestItem.Platform)); + + plan.ItemsToCreate.Should().ContainSingle(); + plan.ItemsToCreate[0].Platforms.Should().ContainSingle().Which.Platform.Should().Be("PS3"); + plan.OwnedCopiesAdded.Should().Be(1); + } + + [Fact] + public void FindImportedReferences_ReturnsOnlyNonNullReferences() + { + var existingBooks = new List + { + Book("Book A", new OwnedVersionModel { Reference = AmazonImportMergeService.FormatOrderReference(SomeOrderId, SomeAsin) }), + Book("Book B", new OwnedVersionModel { Reference = "Bought at a flea market" }), + Book("Book C", new OwnedVersionModel { Reference = null }) + }; + + var result = OwnedItemImportMergeService.FindImportedReferences(existingBooks, b => b.OwnedVersions.Select(v => v.Reference)); + + result.Should().BeEquivalentTo([AmazonImportMergeService.FormatOrderReference(SomeOrderId, SomeAsin), "Bought at a flea market"]); + } + + [Fact] + public void FindImportedReferences_WorksAcrossVideoGamePlatformsToo() + { + var reference = AmazonImportMergeService.FormatOrderReference(SomeOrderId, SomeAsin); + var existingGames = new List + { + Game("Game A", new VideoGamePlatformModel { Platform = "PS5", Reference = reference }), + Game("Game B", new VideoGamePlatformModel { Platform = "PC", Reference = null }) + }; + + var result = OwnedItemImportMergeService.FindImportedReferences(existingGames, g => g.Platforms.Select(p => p.Reference)); + + result.Should().BeEquivalentTo([reference]); + } +}