Should probably just let Claude write the commit messages too

This commit is contained in:
2025-07-11 08:44:07 -04:00
parent 4dec00d283
commit 370c29a6fc
13 changed files with 1416 additions and 27 deletions

View File

@@ -53,7 +53,7 @@ This is a GitHub Actions monitoring dashboard with a React frontend and Express
- Live configuration reloading via file watching - Live configuration reloading via file watching
- Environment variable fallbacks for deployment - Environment variable fallbacks for deployment
- Validation for required GitHub tokens and repository configs - Validation for required GitHub tokens and repository configs
- Optional cache timeout configuration (defaults to 5 minutes) - Optional cache timeout configuration (defaults to 300 seconds)
### API Endpoints ### API Endpoints
- `GET /api/health` - Health check - `GET /api/health` - Health check
@@ -72,7 +72,7 @@ This is a GitHub Actions monitoring dashboard with a React frontend and Express
### Rate Limiting and Caching ### Rate Limiting and Caching
- **Parallel Processing**: API requests are made in parallel with intelligent rate limiting - **Parallel Processing**: API requests are made in parallel with intelligent rate limiting
- **Smart Caching**: Responses are cached with configurable TTL (defaults to 5 minutes) - **Smart Caching**: Responses are cached with configurable TTL (defaults to 300 seconds)
- **Rate Limit Monitoring**: Tracks and respects GitHub's rate limits with automatic waiting - **Rate Limit Monitoring**: Tracks and respects GitHub's rate limits with automatic waiting
- **Controlled Concurrency**: Maximum 10 concurrent requests with 10 requests/second limit - **Controlled Concurrency**: Maximum 10 concurrent requests with 10 requests/second limit
- **Cache Management**: Provides API endpoints to view cache statistics and clear cache when needed - **Cache Management**: Provides API endpoints to view cache statistics and clear cache when needed

View File

@@ -21,6 +21,6 @@
"host": "0.0.0.0" "host": "0.0.0.0"
}, },
"cache": { "cache": {
"timeoutMinutes": 5 "timeoutSeconds": 300
} }
} }

743
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"axios": "^1.6.0", "axios": "^1.6.0",
"chokidar": "^3.5.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
@@ -21,6 +22,9 @@
"tailwindcss": "^3.3.6" "tailwindcss": "^3.3.6"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/react": "^18.2.43", "@types/react": "^18.2.43",
@@ -32,12 +36,20 @@
"eslint": "^8.55.0", "eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.5",
"jsdom": "^26.1.0",
"tsx": "^4.6.2", "tsx": "^4.6.2",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.0.8", "vite": "^5.0.8",
"vitest": "^1.0.4" "vitest": "^1.0.4"
} }
}, },
"node_modules/@adobe/css-tools": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz",
"integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==",
"dev": true,
"license": "MIT"
},
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -64,6 +76,27 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@asamuzakjp/css-color": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@csstools/css-calc": "^2.1.3",
"@csstools/css-color-parser": "^3.0.9",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"lru-cache": "^10.4.3"
}
},
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -375,6 +408,121 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@csstools/color-helpers": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
"integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"engines": {
"node": ">=18"
}
},
"node_modules/@csstools/css-calc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-color-parser": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz",
"integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"dependencies": {
"@csstools/color-helpers": "^5.0.2",
"@csstools/css-calc": "^2.1.4"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -1397,6 +1545,157 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
"chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@testing-library/dom/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@testing-library/dom/node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
"react-is": "^17.0.1"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/@testing-library/dom/node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/@testing-library/jest-dom": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
"integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@adobe/css-tools": "^4.4.0",
"aria-query": "^5.0.0",
"chalk": "^3.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.6.3",
"lodash": "^4.17.21",
"redent": "^3.0.0"
},
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/@testing-library/jest-dom/node_modules/chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/react": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
"integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@testing-library/dom": "^10.0.0",
"@types/react": "^18.0.0 || ^19.0.0",
"@types/react-dom": "^18.0.0 || ^19.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@testing-library/user-event": {
"version": "14.6.1",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
"integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12",
"npm": ">=6"
},
"peerDependencies": {
"@testing-library/dom": ">=7.21.4"
}
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1987,6 +2286,16 @@
"node": ">=0.4.0" "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": { "node_modules/ajv": {
"version": "6.12.6", "version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -2060,6 +2369,16 @@
"dev": true, "dev": true,
"license": "Python-2.0" "license": "Python-2.0"
}, },
"node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"dequal": "^2.0.3"
}
},
"node_modules/array-flatten": { "node_modules/array-flatten": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -2644,6 +2963,13 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true,
"license": "MIT"
},
"node_modules/cssesc": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -2656,6 +2982,20 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/cssstyle": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@asamuzakjp/css-color": "^3.2.0",
"rrweb-cssom": "^0.8.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -2663,6 +3003,20 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/data-urls": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
"dev": true,
"license": "MIT",
"dependencies": {
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/date-fns": { "node_modules/date-fns": {
"version": "2.30.0", "version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@@ -2697,6 +3051,13 @@
} }
} }
}, },
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"dev": true,
"license": "MIT"
},
"node_modules/deep-eql": { "node_modules/deep-eql": {
"version": "4.1.4", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
@@ -2735,6 +3096,16 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/destroy": { "node_modules/destroy": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@@ -2793,6 +3164,14 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/dom-accessibility-api": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.6.1", "version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -2852,6 +3231,19 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"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/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -3790,6 +4182,19 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/html-encoding-sniffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"whatwg-encoding": "^3.1.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -3806,6 +4211,34 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"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/human-signals": { "node_modules/human-signals": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
@@ -3865,6 +4298,16 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/indent-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/inflight": { "node_modules/inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -3968,6 +4411,13 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"dev": true,
"license": "MIT"
},
"node_modules/is-stream": { "node_modules/is-stream": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
@@ -4030,6 +4480,46 @@
"js-yaml": "bin/js-yaml.js" "js-yaml": "bin/js-yaml.js"
} }
}, },
"node_modules/jsdom": {
"version": "26.1.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
"decimal.js": "^10.5.0",
"html-encoding-sniffer": "^4.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.16",
"parse5": "^7.2.1",
"rrweb-cssom": "^0.8.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^5.1.1",
"w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^3.1.1",
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.1.1",
"ws": "^8.18.0",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"canvas": "^3.0.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/jsesc": { "node_modules/jsesc": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -4207,6 +4697,17 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0" "react": "^16.5.1 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.17", "version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
@@ -4328,6 +4829,16 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "9.0.3", "version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
@@ -4477,6 +4988,13 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/nwsapi": {
"version": "2.2.20",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
"integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==",
"dev": true,
"license": "MIT"
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -4614,6 +5132,19 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parseurl": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -5100,6 +5631,20 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/require-directory": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -5217,6 +5762,13 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/rrweb-cssom": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
"dev": true,
"license": "MIT"
},
"node_modules/run-parallel": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -5276,6 +5828,19 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"dev": true,
"license": "ISC",
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.23.2", "version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -5643,6 +6208,19 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"min-indent": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": { "node_modules/strip-json-comments": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -5758,6 +6336,13 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true,
"license": "MIT"
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.17", "version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
@@ -5850,6 +6435,26 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/tldts": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"tldts-core": "^6.1.86"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
"dev": true,
"license": "MIT"
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -5871,6 +6476,32 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/tough-cookie": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"tldts": "^6.1.32"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tree-kill": { "node_modules/tree-kill": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -6659,6 +7290,79 @@
} }
} }
}, },
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -6802,6 +7506,45 @@
"dev": true, "dev": true,
"license": "ISC" "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/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true,
"license": "MIT"
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@@ -11,34 +11,38 @@
"test": "vitest" "test": "vitest"
}, },
"dependencies": { "dependencies": {
"autoprefixer": "^10.4.16",
"axios": "^1.6.0",
"chokidar": "^3.5.3",
"cors": "^2.8.5",
"date-fns": "^2.30.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"lucide-react": "^0.294.0",
"postcss": "^8.4.32",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"axios": "^1.6.0", "tailwindcss": "^3.3.6"
"date-fns": "^2.30.0",
"lucide-react": "^0.294.0",
"tailwindcss": "^3.3.6",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32",
"express": "^4.18.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"chokidar": "^3.5.3"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/react": "^18.2.43", "@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0", "@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"concurrently": "^8.2.2",
"eslint": "^8.55.0", "eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.5",
"jsdom": "^26.1.0",
"tsx": "^4.6.2",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.0.8", "vite": "^5.0.8",
"vitest": "^1.0.4", "vitest": "^1.0.4"
"@types/express": "^4.17.21",
"@types/cors": "^2.8.17",
"tsx": "^4.6.2",
"concurrently": "^8.2.2"
} }
} }

View File

@@ -0,0 +1,267 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { CompactWorkflowCard } from './CompactWorkflowCard';
import { WorkflowRun } from '../types/github';
import { SettingsProvider } from '../contexts/SettingsContext';
// Mock the settings context
const mockSettings = {
githubUsername: 'test-user',
autoRefreshInterval: 30,
showLastUpdateTime: true,
notifications: {
enabled: true,
showFailures: true,
showRecoveries: true,
showWaiting: true
}
};
const MockSettingsProvider = ({ children }: { children: React.ReactNode }) => (
<SettingsProvider>
{children}
</SettingsProvider>
);
const createMockWorkflowRun = (overrides: Partial<WorkflowRun> = {}): WorkflowRun => ({
id: 1,
name: 'Test Workflow',
display_title: 'Test Build',
status: 'completed',
conclusion: 'success',
workflow_id: 123,
head_branch: 'main',
head_sha: 'abc123def456',
run_number: 42,
event: 'push',
created_at: '2024-01-01T10:00:00Z',
updated_at: '2024-01-01T10:05:00Z',
html_url: 'https://github.com/test/repo/actions/runs/1',
repository: {
id: 1,
name: 'test-repo',
full_name: 'test/test-repo',
owner: {
login: 'test-owner',
avatar_url: 'https://github.com/test-owner.png'
}
},
head_commit: {
id: 'abc123def456',
message: 'Add new feature',
author: {
name: 'Test Author',
email: 'test@example.com'
}
},
actor: {
login: 'test-actor',
avatar_url: 'https://github.com/test-actor.png'
},
...overrides
});
describe('CompactWorkflowCard', () => {
it('renders workflow run information correctly', () => {
const run = createMockWorkflowRun();
render(
<MockSettingsProvider>
<CompactWorkflowCard run={run} />
</MockSettingsProvider>
);
expect(screen.getByText('test-repo')).toBeInTheDocument();
expect(screen.getByText('#42')).toBeInTheDocument();
expect(screen.getByText('test-actor')).toBeInTheDocument();
expect(screen.getByText('abc123d')).toBeInTheDocument(); // Shortened commit hash
expect(screen.getByText('Add new feature')).toBeInTheDocument();
});
it('displays success status correctly', () => {
const run = createMockWorkflowRun({
status: 'completed',
conclusion: 'success'
});
render(
<MockSettingsProvider>
<CompactWorkflowCard run={run} />
</MockSettingsProvider>
);
// Check if success icon is rendered (CheckCircle)
const successIcon = screen.getByRole('img', { hidden: true });
expect(successIcon).toBeInTheDocument();
});
it('displays failure status correctly', () => {
const run = createMockWorkflowRun({
status: 'completed',
conclusion: 'failure'
});
render(
<MockSettingsProvider>
<CompactWorkflowCard run={run} />
</MockSettingsProvider>
);
// The card should have failure styling
const card = screen.getByRole('generic');
expect(card).toHaveClass('border-ctp-red/30');
});
it('displays in-progress status correctly', () => {
const run = createMockWorkflowRun({
status: 'in_progress',
conclusion: null
});
render(
<MockSettingsProvider>
<CompactWorkflowCard run={run} />
</MockSettingsProvider>
);
// Check if in-progress icon is rendered
const progressIcon = screen.getByRole('img', { hidden: true });
expect(progressIcon).toBeInTheDocument();
});
it('displays queued status correctly', () => {
const run = createMockWorkflowRun({
status: 'queued',
conclusion: null
});
render(
<MockSettingsProvider>
<CompactWorkflowCard run={run} />
</MockSettingsProvider>
);
// Check if queued icon is rendered
const queuedIcon = screen.getByRole('img', { hidden: true });
expect(queuedIcon).toBeInTheDocument();
});
it('shows actor avatar instead of repository owner avatar', () => {
const run = createMockWorkflowRun();
render(
<MockSettingsProvider>
<CompactWorkflowCard run={run} />
</MockSettingsProvider>
);
const avatar = screen.getByAltText('test-actor');
expect(avatar).toHaveAttribute('src', 'https://github.com/test-actor.png');
});
it('handles click events', () => {
const mockOnClick = vi.fn();
const run = createMockWorkflowRun();
render(
<MockSettingsProvider>
<CompactWorkflowCard run={run} onClick={mockOnClick} />
</MockSettingsProvider>
);
const card = screen.getByRole('generic');
fireEvent.click(card);
expect(mockOnClick).toHaveBeenCalledTimes(1);
});
it('does not trigger onClick when clicking external link', () => {
const mockOnClick = vi.fn();
const run = createMockWorkflowRun();
render(
<MockSettingsProvider>
<CompactWorkflowCard run={run} onClick={mockOnClick} />
</MockSettingsProvider>
);
const externalLink = screen.getByRole('link');
fireEvent.click(externalLink);
expect(mockOnClick).not.toHaveBeenCalled();
});
it('displays external link correctly', () => {
const run = createMockWorkflowRun();
render(
<MockSettingsProvider>
<CompactWorkflowCard run={run} />
</MockSettingsProvider>
);
const externalLink = screen.getByRole('link');
expect(externalLink).toHaveAttribute('href', 'https://github.com/test/repo/actions/runs/1');
expect(externalLink).toHaveAttribute('target', '_blank');
expect(externalLink).toHaveAttribute('rel', 'noopener noreferrer');
});
it('highlights current user failures', () => {
const run = createMockWorkflowRun({
status: 'completed',
conclusion: 'failure',
actor: {
login: 'test-user', // Same as mockSettings.githubUsername
avatar_url: 'https://github.com/test-user.png'
}
});
render(
<MockSettingsProvider>
<CompactWorkflowCard run={run} />
</MockSettingsProvider>
);
// Should have special highlighting for current user failures
const card = screen.getByRole('generic');
expect(card).toHaveClass('ring-2', 'ring-ctp-red/50');
});
it('displays commit information when available', () => {
const run = createMockWorkflowRun({
head_commit: {
id: 'abc123def456',
message: 'Fix critical bug',
author: {
name: 'Test Author',
email: 'test@example.com'
}
}
});
render(
<MockSettingsProvider>
<CompactWorkflowCard run={run} />
</MockSettingsProvider>
);
expect(screen.getByText('abc123d')).toBeInTheDocument();
expect(screen.getByText('Fix critical bug')).toBeInTheDocument();
});
it('handles missing commit information gracefully', () => {
const run = createMockWorkflowRun({
head_commit: null as any
});
render(
<MockSettingsProvider>
<CompactWorkflowCard run={run} />
</MockSettingsProvider>
);
// Should not crash and should still render other information
expect(screen.getByText('test-repo')).toBeInTheDocument();
expect(screen.getByText('#42')).toBeInTheDocument();
});
});

View File

@@ -118,8 +118,8 @@ export const CompactWorkflowCard: React.FC<CompactWorkflowCardProps> = ({ run, o
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-3">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<img <img
src={run.repository.owner.avatar_url} src={run.actor.avatar_url}
alt={run.repository.owner.login} alt={run.actor.login}
className="w-6 h-6 rounded-full" className="w-6 h-6 rounded-full"
/> />
<h3 className="font-semibold text-ctp-text text-sm truncate"> <h3 className="font-semibold text-ctp-text text-sm truncate">

View File

@@ -134,6 +134,13 @@ export const Dashboard: React.FC<DashboardProps> = () => {
}, [settings.autoRefreshInterval, fetchWorkflowRuns]); }, [settings.autoRefreshInterval, fetchWorkflowRuns]);
// Sort workflow runs by created_at date (newest first)
const sortedWorkflowRuns = useMemo(() => {
return [...workflowRuns].sort((a, b) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});
}, [workflowRuns]);
const statusCounts = { const statusCounts = {
total: workflowRuns.length, total: workflowRuns.length,
success: workflowRuns.filter(r => r.conclusion === 'success').length, success: workflowRuns.filter(r => r.conclusion === 'success').length,
@@ -237,13 +244,13 @@ export const Dashboard: React.FC<DashboardProps> = () => {
<div className="col-span-full flex justify-center py-12"> <div className="col-span-full flex justify-center py-12">
<RefreshCw className="w-8 h-8 animate-spin text-ctp-blue" /> <RefreshCw className="w-8 h-8 animate-spin text-ctp-blue" />
</div> </div>
) : workflowRuns.length === 0 ? ( ) : sortedWorkflowRuns.length === 0 ? (
<div className="col-span-full text-center py-12"> <div className="col-span-full text-center py-12">
<p className="text-ctp-subtext0">No workflow runs found.</p> <p className="text-ctp-subtext0">No workflow runs found.</p>
<p className="text-sm text-ctp-subtext1 mt-2">Check your config.json file</p> <p className="text-sm text-ctp-subtext1 mt-2">Check your config.json file</p>
</div> </div>
) : ( ) : (
workflowRuns.map(run => ( sortedWorkflowRuns.map(run => (
<CompactWorkflowCard <CompactWorkflowCard
key={run.id} key={run.id}
run={run} run={run}

View File

@@ -105,8 +105,8 @@ export const WorkflowRunCard: React.FC<WorkflowRunCardProps> = ({ run }) => {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<img <img
src={run.repository.owner.avatar_url} src={run.actor.avatar_url}
alt={run.repository.owner.login} alt={run.actor.login}
className="w-8 h-8 rounded-full" className="w-8 h-8 rounded-full"
/> />
</div> </div>

View File

@@ -90,6 +90,13 @@ export const WorkflowRunsModal: React.FC<WorkflowRunsModalProps> = ({ isOpen, on
} }
}, [isOpen, repository, fetchWorkflowRuns]); }, [isOpen, repository, fetchWorkflowRuns]);
// Sort workflow runs by created_at date (newest first)
const sortedRuns = useMemo(() => {
return [...runs].sort((a, b) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});
}, [runs]);
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
@@ -122,13 +129,13 @@ export const WorkflowRunsModal: React.FC<WorkflowRunsModalProps> = ({ isOpen, on
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-ctp-red">{error}</p> <p className="text-ctp-red">{error}</p>
</div> </div>
) : runs.length === 0 ? ( ) : sortedRuns.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-ctp-subtext0">No workflow runs found</p> <p className="text-ctp-subtext0">No workflow runs found</p>
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{runs.map((run) => ( {sortedRuns.map((run) => (
<div <div
key={run.id} key={run.id}
className="bg-ctp-mantle border border-ctp-surface0 rounded-lg p-4 hover:shadow-sm transition-shadow" className="bg-ctp-mantle border border-ctp-surface0 rounded-lg p-4 hover:shadow-sm transition-shadow"

186
src/services/api.test.ts Normal file
View File

@@ -0,0 +1,186 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import axios from 'axios';
import { ApiService } from './api';
// Mock axios
vi.mock('axios');
const mockedAxios = vi.mocked(axios);
describe('ApiService', () => {
let apiService: ApiService;
beforeEach(() => {
apiService = new ApiService();
vi.clearAllMocks();
});
describe('getWorkflowRuns', () => {
it('should fetch workflow runs successfully', async () => {
const mockRuns = [
{
id: 1,
name: 'Test Workflow',
display_title: 'Test Run',
status: 'completed',
conclusion: 'success',
created_at: '2024-01-01T10:00:00Z',
repository: {
id: 1,
name: 'test-repo',
full_name: 'test/test-repo',
owner: {
login: 'test-owner',
avatar_url: 'https://github.com/test-owner.png'
}
},
actor: {
login: 'test-actor',
avatar_url: 'https://github.com/test-actor.png'
}
}
];
mockedAxios.get.mockResolvedValueOnce({ data: mockRuns });
const result = await apiService.getWorkflowRuns();
expect(mockedAxios.get).toHaveBeenCalledWith('/api/workflow-runs');
expect(result).toEqual(mockRuns);
});
it('should handle API errors', async () => {
const errorMessage = 'Network Error';
mockedAxios.get.mockRejectedValueOnce(new Error(errorMessage));
await expect(apiService.getWorkflowRuns()).rejects.toThrow(errorMessage);
});
});
describe('getRepositoryWorkflowRuns', () => {
it('should fetch repository-specific workflow runs', async () => {
const mockRuns = [
{
id: 1,
name: 'Test Workflow',
display_title: 'Test Run',
status: 'completed',
conclusion: 'success',
created_at: '2024-01-01T10:00:00Z'
}
];
mockedAxios.get.mockResolvedValueOnce({ data: mockRuns });
const result = await apiService.getRepositoryWorkflowRuns('test-owner', 'test-repo');
expect(mockedAxios.get).toHaveBeenCalledWith('/api/repository/test-owner/test-repo/workflow-runs', {
params: { limit: 10 }
});
expect(result).toEqual(mockRuns);
});
it('should handle repository not found', async () => {
mockedAxios.get.mockRejectedValueOnce({
response: { status: 404 }
});
await expect(apiService.getRepositoryWorkflowRuns('invalid', 'repo')).rejects.toMatchObject({
response: { status: 404 }
});
});
});
describe('getConfig', () => {
it('should fetch configuration successfully', async () => {
const mockConfig = {
repositories: [
{
owner: 'test-owner',
name: 'test-repo',
full_name: 'test-owner/test-repo'
}
]
};
mockedAxios.get.mockResolvedValueOnce({ data: mockConfig });
const result = await apiService.getConfig();
expect(mockedAxios.get).toHaveBeenCalledWith('/api/config');
expect(result).toEqual(mockConfig);
});
});
describe('getRateLimit', () => {
it('should fetch rate limit information', async () => {
const mockRateLimit = {
limit: 5000,
remaining: 4999,
resetTime: 1640995200000,
used: 1,
resetTimeFormatted: '2022-01-01T00:00:00.000Z',
timeUntilReset: 3600000
};
mockedAxios.get.mockResolvedValueOnce({ data: mockRateLimit });
const result = await apiService.getRateLimitInfo();
expect(mockedAxios.get).toHaveBeenCalledWith('/api/rate-limit');
expect(result).toEqual(mockRateLimit);
});
});
describe('getCacheStats', () => {
it('should fetch cache statistics', async () => {
const mockStats = {
size: 10,
entries: ['key1', 'key2', 'key3']
};
mockedAxios.get.mockResolvedValueOnce({ data: mockStats });
const result = await apiService.getCacheStats();
expect(mockedAxios.get).toHaveBeenCalledWith('/api/cache/stats');
expect(result).toEqual(mockStats);
});
});
describe('clearCache', () => {
it('should clear cache successfully', async () => {
const mockResponse = { message: 'Cache cleared successfully' };
mockedAxios.delete.mockResolvedValueOnce({ data: mockResponse });
const result = await apiService.clearCache();
expect(mockedAxios.delete).toHaveBeenCalledWith('/api/cache');
expect(result).toEqual(mockResponse);
});
});
describe('error handling', () => {
it('should handle network errors', async () => {
mockedAxios.get.mockRejectedValueOnce(new Error('Network Error'));
await expect(apiService.getWorkflowRuns()).rejects.toThrow('Network Error');
});
it('should handle HTTP errors', async () => {
mockedAxios.get.mockRejectedValueOnce({
response: {
status: 500,
data: { error: 'Internal Server Error' }
}
});
await expect(apiService.getWorkflowRuns()).rejects.toMatchObject({
response: {
status: 500,
data: { error: 'Internal Server Error' }
}
});
});
});
});

42
src/test-setup.ts Normal file
View File

@@ -0,0 +1,42 @@
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from '@testing-library/jest-dom/matchers';
// Extend Vitest's expect with testing-library matchers
expect.extend(matchers);
// Clean up after each test
afterEach(() => {
cleanup();
});
// Mock window.matchMedia for components that use it
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => {},
}),
});
// Mock IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
constructor() {}
disconnect() {}
observe() {}
unobserve() {}
} as any;
// Mock ResizeObserver
global.ResizeObserver = class ResizeObserver {
constructor() {}
disconnect() {}
observe() {}
unobserve() {}
} as any;

116
src/utils/sorting.test.ts Normal file
View File

@@ -0,0 +1,116 @@
import { describe, it, expect } from 'vitest';
import { WorkflowRun } from '../types/github';
// Helper function to create a mock workflow run
const createMockWorkflowRun = (id: number, created_at: string): WorkflowRun => ({
id,
name: `Test Workflow ${id}`,
display_title: `Test ${id}`,
status: 'completed',
conclusion: 'success',
workflow_id: 1,
head_branch: 'main',
head_sha: 'abc123',
run_number: id,
event: 'push',
created_at,
updated_at: created_at,
html_url: `https://github.com/test/repo/actions/runs/${id}`,
repository: {
id: 1,
name: 'test-repo',
full_name: 'test/test-repo',
owner: {
login: 'test-owner',
avatar_url: 'https://github.com/test-owner.png'
}
},
head_commit: {
id: 'abc123',
message: 'Test commit',
author: {
name: 'Test Author',
email: 'test@example.com'
}
},
actor: {
login: 'test-actor',
avatar_url: 'https://github.com/test-actor.png'
}
});
// Sorting function (extracted from components)
const sortWorkflowRunsByDate = (runs: WorkflowRun[]): WorkflowRun[] => {
return [...runs].sort((a, b) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});
};
describe('Workflow Run Sorting', () => {
it('should sort workflow runs by created_at date (newest first)', () => {
const runs = [
createMockWorkflowRun(1, '2024-01-01T10:00:00Z'),
createMockWorkflowRun(2, '2024-01-03T10:00:00Z'),
createMockWorkflowRun(3, '2024-01-02T10:00:00Z')
];
const sortedRuns = sortWorkflowRunsByDate(runs);
expect(sortedRuns).toHaveLength(3);
expect(sortedRuns[0].id).toBe(2); // 2024-01-03 (newest)
expect(sortedRuns[1].id).toBe(3); // 2024-01-02
expect(sortedRuns[2].id).toBe(1); // 2024-01-01 (oldest)
});
it('should handle empty array', () => {
const runs: WorkflowRun[] = [];
const sortedRuns = sortWorkflowRunsByDate(runs);
expect(sortedRuns).toHaveLength(0);
});
it('should handle single item', () => {
const runs = [createMockWorkflowRun(1, '2024-01-01T10:00:00Z')];
const sortedRuns = sortWorkflowRunsByDate(runs);
expect(sortedRuns).toHaveLength(1);
expect(sortedRuns[0].id).toBe(1);
});
it('should handle identical timestamps', () => {
const timestamp = '2024-01-01T10:00:00Z';
const runs = [
createMockWorkflowRun(1, timestamp),
createMockWorkflowRun(2, timestamp),
createMockWorkflowRun(3, timestamp)
];
const sortedRuns = sortWorkflowRunsByDate(runs);
expect(sortedRuns).toHaveLength(3);
// Order should be stable for identical timestamps
});
it('should not mutate the original array', () => {
const runs = [
createMockWorkflowRun(1, '2024-01-01T10:00:00Z'),
createMockWorkflowRun(2, '2024-01-02T10:00:00Z')
];
const originalOrder = runs.map(r => r.id);
sortWorkflowRunsByDate(runs);
expect(runs.map(r => r.id)).toEqual(originalOrder);
});
it('should handle various date formats', () => {
const runs = [
createMockWorkflowRun(1, '2024-01-01T10:00:00.000Z'),
createMockWorkflowRun(2, '2024-01-02T10:00:00Z'),
createMockWorkflowRun(3, '2024-01-01T15:00:00.500Z')
];
const sortedRuns = sortWorkflowRunsByDate(runs);
expect(sortedRuns[0].id).toBe(2); // 2024-01-02
expect(sortedRuns[1].id).toBe(3); // 2024-01-01 15:00
expect(sortedRuns[2].id).toBe(1); // 2024-01-01 10:00
});
});

17
vitest.config.ts Normal file
View File

@@ -0,0 +1,17 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: ['src/test-setup.ts'],
globals: true,
css: true,
},
resolve: {
alias: {
'@': '/src',
},
},
});