From 8db2fa2aab750ae99faf84596d7b5e74e917b0c0 Mon Sep 17 00:00:00 2001 From: yoyuzh Date: Sat, 14 Mar 2026 11:03:07 +0800 Subject: [PATCH] add backend --- .gemini/settings.json | 16 + .gitignore | 8 + backend/README.md | 71 + backend/dev-backend.err.log | 1 + backend/dev-backend.log | 1772 ++++++ backend/pom.xml | 108 + backend/sql/mysql-init.sql | 54 + backend/sql/opengauss-init.sql | 46 + .../com/yoyuzh/PortalBackendApplication.java | 21 + .../java/com/yoyuzh/auth/AuthController.java | 33 + .../java/com/yoyuzh/auth/AuthService.java | 83 + .../yoyuzh/auth/CustomUserDetailsService.java | 31 + .../com/yoyuzh/auth/DevAuthController.java | 26 + .../com/yoyuzh/auth/JwtTokenProvider.java | 62 + .../src/main/java/com/yoyuzh/auth/User.java | 84 + .../java/com/yoyuzh/auth/UserController.java | 24 + .../java/com/yoyuzh/auth/UserRepository.java | 13 + .../com/yoyuzh/auth/dto/AuthResponse.java | 4 + .../com/yoyuzh/auth/dto/LoginRequest.java | 9 + .../com/yoyuzh/auth/dto/RegisterRequest.java | 12 + .../yoyuzh/auth/dto/UserProfileResponse.java | 6 + .../java/com/yoyuzh/common/ApiResponse.java | 16 + .../com/yoyuzh/common/BusinessException.java | 15 + .../java/com/yoyuzh/common/ErrorCode.java | 18 + .../yoyuzh/common/GlobalExceptionHandler.java | 51 + .../java/com/yoyuzh/common/PageResponse.java | 6 + .../com/yoyuzh/config/CquApiProperties.java | 35 + .../yoyuzh/config/FileStorageProperties.java | 26 + .../config/JwtAuthenticationFilter.java | 45 + .../java/com/yoyuzh/config/JwtProperties.java | 26 + .../java/com/yoyuzh/config/OpenApiConfig.java | 26 + .../com/yoyuzh/config/RestClientConfig.java | 14 + .../com/yoyuzh/config/SecurityConfig.java | 83 + .../src/main/java/com/yoyuzh/cqu/Course.java | 154 + .../java/com/yoyuzh/cqu/CourseRepository.java | 11 + .../java/com/yoyuzh/cqu/CourseResponse.java | 11 + .../java/com/yoyuzh/cqu/CquApiClient.java | 40 + .../java/com/yoyuzh/cqu/CquController.java | 44 + .../java/com/yoyuzh/cqu/CquDataService.java | 129 + .../com/yoyuzh/cqu/CquMockDataFactory.java | 68 + .../src/main/java/com/yoyuzh/cqu/Grade.java | 110 + .../java/com/yoyuzh/cqu/GradeRepository.java | 11 + .../java/com/yoyuzh/cqu/GradeResponse.java | 8 + .../java/com/yoyuzh/files/FileController.java | 77 + .../yoyuzh/files/FileMetadataResponse.java | 14 + .../java/com/yoyuzh/files/FileService.java | 216 + .../java/com/yoyuzh/files/MkdirRequest.java | 6 + .../java/com/yoyuzh/files/StoredFile.java | 132 + .../yoyuzh/files/StoredFileRepository.java | 32 + .../src/main/resources/application-dev.yml | 17 + backend/src/main/resources/application.yml | 41 + backend/src/main/resources/logback.xml | 14 + .../java/com/yoyuzh/auth/AuthServiceTest.java | 105 + .../com/yoyuzh/cqu/CquDataServiceTest.java | 86 + .../yoyuzh/cqu/CquMockDataFactoryTest.java | 29 + .../com/yoyuzh/files/FileServiceTest.java | 114 + front/.env.example | 9 + front/.gitignore | 8 + front/.vite/deps/_metadata.json | 8 + front/.vite/deps/package.json | 3 + front/README.md | 20 + {vue => front}/index.html | 8 +- front/metadata.json | 5 + front/package-lock.json | 5281 +++++++++++++++++ front/package.json | 39 + front/src/App.tsx | 25 + front/src/components/layout/Layout.tsx | 91 + front/src/components/ui/button.tsx | 36 + front/src/components/ui/card.tsx | 78 + front/src/components/ui/input.tsx | 24 + front/src/index.css | 88 + front/src/lib/utils.ts | 6 + front/src/main.tsx | 10 + front/src/pages/Files.tsx | 275 + front/src/pages/Games.tsx | 109 + front/src/pages/Login.tsx | 130 + front/src/pages/Overview.tsx | 208 + front/src/pages/School.tsx | 291 + front/tsconfig.json | 26 + front/vite.config.ts | 24 + scripts/local-smoke.ps1 | 130 + scripts/start-backend-dev.ps1 | 37 + scripts/start-frontend-dev.ps1 | 36 + todo_list.md | 267 - vue/.gitignore | 24 - vue/.vscode/extensions.json | 3 - vue/README.md | 11 - vue/package-lock.json | 1834 ------ vue/package.json | 32 - vue/public/race/audio.js | 178 - vue/public/race/debug.js | 261 - vue/public/race/draw.js | 472 -- vue/public/race/game.js | 433 -- vue/public/race/generative.js | 1057 ---- vue/public/race/hud.js | 168 - vue/public/race/index.html | 36 - vue/public/race/input.js | 402 -- vue/public/race/levels.js | 447 -- vue/public/race/main.js | 41 - vue/public/race/release.js | 16 - vue/public/race/releaseJS13K.js | 15 - vue/public/race/scene.js | 120 - vue/public/race/sounds.js | 9 - vue/public/race/track.js | 425 -- vue/public/race/trackGen.js | 274 - vue/public/race/utilities.js | 229 - vue/public/race/vehicle.js | 625 -- vue/public/race/webgl.js | 305 - vue/public/t_race/favicon.png | Bin 10912 -> 0 bytes vue/public/t_race/index.html | 1 - vue/public/vite.svg | 1 - vue/src/App.vue | 1204 ---- vue/src/assets/vue.svg | 1 - vue/src/components/HelloWorld.vue | 41 - vue/src/lighting.spec.ts | 47 - vue/src/lighting.ts | 67 - vue/src/main.ts | 5 - vue/src/style.css | 2215 ------- vue/src/theme.spec.ts | 29 - vue/src/theme.ts | 27 - vue/tsconfig.app.json | 16 - vue/tsconfig.json | 7 - vue/tsconfig.node.json | 26 - vue/vite.config.ts | 7 - 模板/index.html | 54 + 模板/style.css | 236 + 草图/pencil-new-前端页面需求文档.md | 668 +++ 草图/pencil-new.pen | 2697 +++++++++ 需求文档.md | 182 + 需求文档_web_desktop_prd.md | 479 -- 130 files changed, 15152 insertions(+), 11861 deletions(-) create mode 100644 .gemini/settings.json create mode 100644 .gitignore create mode 100644 backend/README.md create mode 100644 backend/dev-backend.err.log create mode 100644 backend/dev-backend.log create mode 100644 backend/pom.xml create mode 100644 backend/sql/mysql-init.sql create mode 100644 backend/sql/opengauss-init.sql create mode 100644 backend/src/main/java/com/yoyuzh/PortalBackendApplication.java create mode 100644 backend/src/main/java/com/yoyuzh/auth/AuthController.java create mode 100644 backend/src/main/java/com/yoyuzh/auth/AuthService.java create mode 100644 backend/src/main/java/com/yoyuzh/auth/CustomUserDetailsService.java create mode 100644 backend/src/main/java/com/yoyuzh/auth/DevAuthController.java create mode 100644 backend/src/main/java/com/yoyuzh/auth/JwtTokenProvider.java create mode 100644 backend/src/main/java/com/yoyuzh/auth/User.java create mode 100644 backend/src/main/java/com/yoyuzh/auth/UserController.java create mode 100644 backend/src/main/java/com/yoyuzh/auth/UserRepository.java create mode 100644 backend/src/main/java/com/yoyuzh/auth/dto/AuthResponse.java create mode 100644 backend/src/main/java/com/yoyuzh/auth/dto/LoginRequest.java create mode 100644 backend/src/main/java/com/yoyuzh/auth/dto/RegisterRequest.java create mode 100644 backend/src/main/java/com/yoyuzh/auth/dto/UserProfileResponse.java create mode 100644 backend/src/main/java/com/yoyuzh/common/ApiResponse.java create mode 100644 backend/src/main/java/com/yoyuzh/common/BusinessException.java create mode 100644 backend/src/main/java/com/yoyuzh/common/ErrorCode.java create mode 100644 backend/src/main/java/com/yoyuzh/common/GlobalExceptionHandler.java create mode 100644 backend/src/main/java/com/yoyuzh/common/PageResponse.java create mode 100644 backend/src/main/java/com/yoyuzh/config/CquApiProperties.java create mode 100644 backend/src/main/java/com/yoyuzh/config/FileStorageProperties.java create mode 100644 backend/src/main/java/com/yoyuzh/config/JwtAuthenticationFilter.java create mode 100644 backend/src/main/java/com/yoyuzh/config/JwtProperties.java create mode 100644 backend/src/main/java/com/yoyuzh/config/OpenApiConfig.java create mode 100644 backend/src/main/java/com/yoyuzh/config/RestClientConfig.java create mode 100644 backend/src/main/java/com/yoyuzh/config/SecurityConfig.java create mode 100644 backend/src/main/java/com/yoyuzh/cqu/Course.java create mode 100644 backend/src/main/java/com/yoyuzh/cqu/CourseRepository.java create mode 100644 backend/src/main/java/com/yoyuzh/cqu/CourseResponse.java create mode 100644 backend/src/main/java/com/yoyuzh/cqu/CquApiClient.java create mode 100644 backend/src/main/java/com/yoyuzh/cqu/CquController.java create mode 100644 backend/src/main/java/com/yoyuzh/cqu/CquDataService.java create mode 100644 backend/src/main/java/com/yoyuzh/cqu/CquMockDataFactory.java create mode 100644 backend/src/main/java/com/yoyuzh/cqu/Grade.java create mode 100644 backend/src/main/java/com/yoyuzh/cqu/GradeRepository.java create mode 100644 backend/src/main/java/com/yoyuzh/cqu/GradeResponse.java create mode 100644 backend/src/main/java/com/yoyuzh/files/FileController.java create mode 100644 backend/src/main/java/com/yoyuzh/files/FileMetadataResponse.java create mode 100644 backend/src/main/java/com/yoyuzh/files/FileService.java create mode 100644 backend/src/main/java/com/yoyuzh/files/MkdirRequest.java create mode 100644 backend/src/main/java/com/yoyuzh/files/StoredFile.java create mode 100644 backend/src/main/java/com/yoyuzh/files/StoredFileRepository.java create mode 100644 backend/src/main/resources/application-dev.yml create mode 100644 backend/src/main/resources/application.yml create mode 100644 backend/src/main/resources/logback.xml create mode 100644 backend/src/test/java/com/yoyuzh/auth/AuthServiceTest.java create mode 100644 backend/src/test/java/com/yoyuzh/cqu/CquDataServiceTest.java create mode 100644 backend/src/test/java/com/yoyuzh/cqu/CquMockDataFactoryTest.java create mode 100644 backend/src/test/java/com/yoyuzh/files/FileServiceTest.java create mode 100644 front/.env.example create mode 100644 front/.gitignore create mode 100644 front/.vite/deps/_metadata.json create mode 100644 front/.vite/deps/package.json create mode 100644 front/README.md rename {vue => front}/index.html (52%) create mode 100644 front/metadata.json create mode 100644 front/package-lock.json create mode 100644 front/package.json create mode 100644 front/src/App.tsx create mode 100644 front/src/components/layout/Layout.tsx create mode 100644 front/src/components/ui/button.tsx create mode 100644 front/src/components/ui/card.tsx create mode 100644 front/src/components/ui/input.tsx create mode 100644 front/src/index.css create mode 100644 front/src/lib/utils.ts create mode 100644 front/src/main.tsx create mode 100644 front/src/pages/Files.tsx create mode 100644 front/src/pages/Games.tsx create mode 100644 front/src/pages/Login.tsx create mode 100644 front/src/pages/Overview.tsx create mode 100644 front/src/pages/School.tsx create mode 100644 front/tsconfig.json create mode 100644 front/vite.config.ts create mode 100644 scripts/local-smoke.ps1 create mode 100644 scripts/start-backend-dev.ps1 create mode 100644 scripts/start-frontend-dev.ps1 delete mode 100644 todo_list.md delete mode 100644 vue/.gitignore delete mode 100644 vue/.vscode/extensions.json delete mode 100644 vue/README.md delete mode 100644 vue/package-lock.json delete mode 100644 vue/package.json delete mode 100644 vue/public/race/audio.js delete mode 100644 vue/public/race/debug.js delete mode 100644 vue/public/race/draw.js delete mode 100644 vue/public/race/game.js delete mode 100644 vue/public/race/generative.js delete mode 100644 vue/public/race/hud.js delete mode 100644 vue/public/race/index.html delete mode 100644 vue/public/race/input.js delete mode 100644 vue/public/race/levels.js delete mode 100644 vue/public/race/main.js delete mode 100644 vue/public/race/release.js delete mode 100644 vue/public/race/releaseJS13K.js delete mode 100644 vue/public/race/scene.js delete mode 100644 vue/public/race/sounds.js delete mode 100644 vue/public/race/track.js delete mode 100644 vue/public/race/trackGen.js delete mode 100644 vue/public/race/utilities.js delete mode 100644 vue/public/race/vehicle.js delete mode 100644 vue/public/race/webgl.js delete mode 100644 vue/public/t_race/favicon.png delete mode 100644 vue/public/t_race/index.html delete mode 100644 vue/public/vite.svg delete mode 100644 vue/src/App.vue delete mode 100644 vue/src/assets/vue.svg delete mode 100644 vue/src/components/HelloWorld.vue delete mode 100644 vue/src/lighting.spec.ts delete mode 100644 vue/src/lighting.ts delete mode 100644 vue/src/main.ts delete mode 100644 vue/src/style.css delete mode 100644 vue/src/theme.spec.ts delete mode 100644 vue/src/theme.ts delete mode 100644 vue/tsconfig.app.json delete mode 100644 vue/tsconfig.json delete mode 100644 vue/tsconfig.node.json delete mode 100644 vue/vite.config.ts create mode 100644 模板/index.html create mode 100644 模板/style.css create mode 100644 草图/pencil-new-前端页面需求文档.md create mode 100644 草图/pencil-new.pen create mode 100644 需求文档.md delete mode 100644 需求文档_web_desktop_prd.md diff --git a/.gemini/settings.json b/.gemini/settings.json new file mode 100644 index 0000000..666b003 --- /dev/null +++ b/.gemini/settings.json @@ -0,0 +1,16 @@ +{ + "general": { + "defaultApprovalMode": "plan" + }, + "ui": { + "footer": { + "hideModelInfo": false, + "hideContextPercentage": false + }, + "showMemoryUsage": true, + "showModelInfoInChat": true + }, + "model": { + "name": "gemini-3-pro" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1231c0e --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +backend/target/ +data/ +storage/ +backend-dev.out.log +backend-dev.err.log +frontend-dev.out.log +frontend-dev.err.log +vue/dist/ diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..7742c10 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,71 @@ +# yoyuzh-portal-backend + +`yoyuzh.xyz` 的 Spring Boot 3.x 后端,提供: + +- 用户注册、登录、JWT 鉴权、用户信息接口 +- 个人网盘上传、下载、删除、目录管理、分页列表 +- CQU 课表与成绩聚合接口 +- Swagger 文档、统一异常、日志输出 + +## 环境要求 + +- JDK 17+ +- Maven 3.9+ +- 生产环境使用 MySQL 8.x 或 openGauss + +## 启动 + +默认配置: + +```bash +mvn spring-boot:run +``` + +本地联调建议使用 `dev` 环境: + +```bash +mvn spring-boot:run -Dspring-boot.run.profiles=dev +``` + +`dev` 环境特点: + +- 数据库使用 H2 文件库 +- CQU 接口返回 mock 数据 +- 方便和 `vue/` 前端直接联调 + +## 访问地址 + +- Swagger: `http://localhost:8080/swagger-ui.html` +- H2 Console: `http://localhost:8080/h2-console`(仅 `dev` 环境) + +## 数据库脚本 + +- MySQL: `sql/mysql-init.sql` +- openGauss: `sql/opengauss-init.sql` + +## 主要接口 + +- `POST /api/auth/register` +- `POST /api/auth/login` +- `GET /api/user/profile` +- `POST /api/files/upload` +- `POST /api/files/mkdir` +- `GET /api/files/list` +- `GET /api/files/download/{fileId}` +- `DELETE /api/files/{fileId}` +- `GET /api/cqu/schedule` +- `GET /api/cqu/grades` + +## CQU 配置 + +部署到真实环境时修改: + +```yaml +app: + cqu: + base-url: https://your-cqu-api + require-login: false + mock-enabled: false +``` + +当前 Java 后端保留了 HTTP 适配点;本地 `dev` 环境使用 mock 数据先把前后端链路跑通。 diff --git a/backend/dev-backend.err.log b/backend/dev-backend.err.log new file mode 100644 index 0000000..3438401 --- /dev/null +++ b/backend/dev-backend.err.log @@ -0,0 +1 @@ +^C \ No newline at end of file diff --git a/backend/dev-backend.log b/backend/dev-backend.log new file mode 100644 index 0000000..afbf1be --- /dev/null +++ b/backend/dev-backend.log @@ -0,0 +1,1772 @@ +[INFO] Scanning for projects... +[INFO] +[INFO] ------------------< com.yoyuzh:yoyuzh-portal-backend >------------------ +[INFO] Building yoyuzh-portal-backend 0.0.1-SNAPSHOT +[INFO] from pom.xml +[INFO] --------------------------------[ jar ]--------------------------------- +[INFO] +[INFO] >>> spring-boot:3.3.8:run (default-cli) > test-compile @ yoyuzh-portal-backend >>> +[INFO] +[INFO] --- resources:3.3.1:resources (default-resources) @ yoyuzh-portal-backend --- +[INFO] Copying 2 resources from src\main\resources to target\classes +[INFO] Copying 1 resource from src\main\resources to target\classes +[INFO] +[INFO] --- compiler:3.13.0:compile (default-compile) @ yoyuzh-portal-backend --- +[INFO] Nothing to compile - all classes are up to date. +[INFO] +[INFO] --- resources:3.3.1:testResources (default-testResources) @ yoyuzh-portal-backend --- +[INFO] skip non existing resourceDirectory C:\Users\yoyuz\Documents\code\my_site\backend\src\test\resources +[INFO] +[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ yoyuzh-portal-backend --- +[INFO] Nothing to compile - all classes are up to date. +[INFO] +[INFO] <<< spring-boot:3.3.8:run (default-cli) < test-compile @ yoyuzh-portal-backend <<< +[INFO] +[INFO] +[INFO] --- spring-boot:3.3.8:run (default-cli) @ yoyuzh-portal-backend --- +[INFO] Attaching agents: [] + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + +2026-03-09 19:29:00.171 [background-preinit] INFO o.h.validator.internal.util.Version - HV000001: Hibernate Validator 8.0.2.Final + :: Spring Boot :: (v3.3.8) + +2026-03-09 19:29:00.314 [main] INFO com.yoyuzh.PortalBackendApplication - Starting PortalBackendApplication using Java 22.0.2 with PID 28656 (C:\Users\yoyuz\Documents\code\my_site\backend\target\classes started by yoyuz in C:\Users\yoyuz\Documents\code\my_site\backend) +2026-03-09 19:29:00.317 [main] INFO com.yoyuzh.PortalBackendApplication - The following 1 profile is active: "dev" +2026-03-09 19:29:00.317 [main] DEBUG o.s.boot.SpringApplication - Loading source class com.yoyuzh.PortalBackendApplication +2026-03-09 19:29:00.415 [main] DEBUG o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@56a4479a +2026-03-09 19:29:01.451 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFAULT mode. +2026-03-09 19:29:01.467 [main] DEBUG o.s.b.a.AutoConfigurationPackages - @EnableAutoConfiguration was declared on a class in the package 'com.yoyuzh'. Automatic @Repository and @Entity scanning is enabled. +2026-03-09 19:29:01.526 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 67 ms. Found 4 JPA repository interfaces. +2026-03-09 19:29:02.356 [main] DEBUG o.s.b.w.e.t.TomcatServletWebServerFactory - Code archive: C:\Users\yoyuz\.m2\repository\org\springframework\boot\spring-boot\3.3.8\spring-boot-3.3.8.jar +2026-03-09 19:29:02.358 [main] DEBUG o.s.b.w.e.t.TomcatServletWebServerFactory - Code archive: C:\Users\yoyuz\.m2\repository\org\springframework\boot\spring-boot\3.3.8\spring-boot-3.3.8.jar +2026-03-09 19:29:02.360 [main] DEBUG o.s.b.w.e.t.TomcatServletWebServerFactory - None of the document roots [src/main/webapp, public, static] point to a directory and will be ignored. +2026-03-09 19:29:02.378 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port 8080 (http) +2026-03-09 19:29:02.387 [main] INFO o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"] +2026-03-09 19:29:02.388 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat] +2026-03-09 19:29:02.389 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.34] +2026-03-09 19:29:02.506 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext +2026-03-09 19:29:02.508 [main] DEBUG o.s.b.w.s.c.ServletWebServerApplicationContext - Published root WebApplicationContext as ServletContext attribute with name [org.springframework.web.context.WebApplicationContext.ROOT] +2026-03-09 19:29:02.508 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 2093 ms +2026-03-09 19:29:02.664 [main] INFO o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default] +2026-03-09 19:29:02.723 [main] INFO org.hibernate.Version - HHH000412: Hibernate ORM core version 6.5.3.Final +2026-03-09 19:29:02.753 [main] INFO o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled +2026-03-09 19:29:03.069 [main] INFO o.s.o.j.p.SpringPersistenceUnitInfo - No LoadTimeWeaver setup: ignoring JPA class transformer +2026-03-09 19:29:03.103 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting... +2026-03-09 19:29:07.487 [main] INFO com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection conn1: url=jdbc:h2:file:./data/yoyuzh_portal_dev user=SA +2026-03-09 19:29:07.489 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed. +2026-03-09 19:29:08.584 [main] INFO o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) +2026-03-09 19:29:08.633 [main] DEBUG org.hibernate.SQL - + alter table if exists portal_file + drop constraint if exists uk_file_user_path_name +2026-03-09 19:29:08.635 [main] DEBUG org.hibernate.SQL - + alter table if exists portal_file + add constraint uk_file_user_path_name unique (user_id, path, filename) +2026-03-09 19:29:08.640 [main] DEBUG org.hibernate.SQL - + alter table if exists portal_user + drop constraint if exists uk_user_username +2026-03-09 19:29:08.641 [main] DEBUG org.hibernate.SQL - + alter table if exists portal_user + add constraint uk_user_username unique (username) +2026-03-09 19:29:08.642 [main] DEBUG org.hibernate.SQL - + alter table if exists portal_user + drop constraint if exists uk_user_email +2026-03-09 19:29:08.643 [main] DEBUG org.hibernate.SQL - + alter table if exists portal_user + add constraint uk_user_email unique (email) +2026-03-09 19:29:08.652 [main] INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Initialized JPA EntityManagerFactory for persistence unit 'default' +2026-03-09 19:29:08.946 [main] DEBUG o.s.b.w.s.ServletContextInitializerBeans - Mapping filters: springSecurityFilterChain urls=[/*] order=-100, characterEncodingFilter urls=[/*] order=-2147483648, formContentFilter urls=[/*] order=-9900, requestContextFilter urls=[/*] order=-105, jwtAuthenticationFilter urls=[/*] order=2147483647 +2026-03-09 19:29:08.950 [main] DEBUG o.s.b.w.s.ServletContextInitializerBeans - Mapping servlets: dispatcherServlet urls=[/], h2Console urls=[/h2-console/*] +2026-03-09 19:29:08.970 [main] DEBUG o.s.b.w.s.f.OrderedRequestContextFilter - Filter 'requestContextFilter' configured for use +2026-03-09 19:29:08.970 [main] DEBUG o.s.b.w.s.f.OrderedCharacterEncodingFilter - Filter 'characterEncodingFilter' configured for use +2026-03-09 19:29:08.981 [main] DEBUG o.s.b.w.s.DelegatingFilterProxyRegistrationBean$1 - Filter 'springSecurityFilterChain' configured for use +2026-03-09 19:29:08.981 [main] DEBUG o.s.b.w.s.f.OrderedFormContentFilter - Filter 'formContentFilter' configured for use +2026-03-09 19:29:09.101 [main] INFO o.s.s.c.a.a.c.InitializeAuthenticationProviderBeanManagerConfigurer$InitializeAuthenticationProviderManagerConfigurer - Global AuthenticationManager configured with AuthenticationProvider bean with name authenticationProvider +2026-03-09 19:29:09.101 [main] WARN o.s.s.c.a.a.c.InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer - Global AuthenticationManager configured with an AuthenticationProvider bean. UserDetailsService beans will not be used for username/password login. Consider removing the AuthenticationProvider bean. Alternatively, consider using the UserDetailsService in a manually instantiated DaoAuthenticationProvider. +2026-03-09 19:29:09.277 [main] INFO o.s.d.j.r.query.QueryEnhancerFactory - Hibernate is in classpath; If applicable, HQL parser will be used. +2026-03-09 19:29:10.030 [main] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - 18 mappings in 'requestMappingHandlerMapping' +2026-03-09 19:29:10.064 [main] DEBUG o.s.w.s.h.SimpleUrlHandlerMapping - Patterns [/webjars/**, /**, /swagger-ui*/*swagger-initializer.js, /swagger-ui*/**] in 'resourceHandlerMapping' +2026-03-09 19:29:10.224 [main] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerAdapter - ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice +2026-03-09 19:29:10.274 [main] DEBUG o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver - ControllerAdvice beans: 2 @ExceptionHandler, 1 ResponseBodyAdvice +2026-03-09 19:29:10.399 [main] INFO o.s.b.a.h.H2ConsoleAutoConfiguration - H2 console available at '/h2-console'. Database available at 'jdbc:h2:file:./data/yoyuzh_portal_dev' +2026-03-09 19:29:10.456 [main] INFO o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"] +2026-03-09 19:29:10.477 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat started on port 8080 (http) with context path '/' +2026-03-09 19:29:10.489 [main] DEBUG o.s.b.a.l.ConditionEvaluationReportLogger - + + +============================ +CONDITIONS EVALUATION REPORT +============================ + + +Positive matches: +----------------- + + AopAutoConfiguration matched: + - @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition) + + AopAutoConfiguration.AspectJAutoProxyingConfiguration matched: + - @ConditionalOnClass found required class 'org.aspectj.weaver.Advice' (OnClassCondition) + + AopAutoConfiguration.AspectJAutoProxyingConfiguration.CglibAutoProxyConfiguration matched: + - @ConditionalOnProperty (spring.aop.proxy-target-class=true) matched (OnPropertyCondition) + + ApplicationAvailabilityAutoConfiguration#applicationAvailability matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.availability.ApplicationAvailability; SearchStrategy: all) did not find any beans (OnBeanCondition) + + DataSourceAutoConfiguration matched: + - @ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType' (OnClassCondition) + - @ConditionalOnMissingBean (types: io.r2dbc.spi.ConnectionFactory; SearchStrategy: all) did not find any beans (OnBeanCondition) + + DataSourceAutoConfiguration.PooledDataSourceConfiguration matched: + - AnyNestedCondition 1 matched 1 did not; NestedCondition on DataSourceAutoConfiguration.PooledDataSourceCondition.PooledDataSourceAvailable PooledDataSource found supported DataSource; NestedCondition on DataSourceAutoConfiguration.PooledDataSourceCondition.ExplicitType @ConditionalOnProperty (spring.datasource.type) did not find property 'type' (DataSourceAutoConfiguration.PooledDataSourceCondition) + - @ConditionalOnMissingBean (types: javax.sql.DataSource,javax.sql.XADataSource; SearchStrategy: all) did not find any beans (OnBeanCondition) + + DataSourceAutoConfiguration.PooledDataSourceConfiguration#jdbcConnectionDetails matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; SearchStrategy: all) did not find any beans (OnBeanCondition) + + DataSourceConfiguration.Hikari matched: + - @ConditionalOnClass found required class 'com.zaxxer.hikari.HikariDataSource' (OnClassCondition) + - @ConditionalOnProperty (spring.datasource.type=com.zaxxer.hikari.HikariDataSource) matched (OnPropertyCondition) + - @ConditionalOnMissingBean (types: javax.sql.DataSource; SearchStrategy: all) did not find any beans (OnBeanCondition) + + DataSourceInitializationConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.jdbc.datasource.init.DatabasePopulator' (OnClassCondition) + - @ConditionalOnSingleCandidate (types: javax.sql.DataSource; SearchStrategy: all) found a single bean 'dataSource'; @ConditionalOnMissingBean (types: org.springframework.boot.autoconfigure.sql.init.SqlDataSourceScriptDatabaseInitializer,org.springframework.boot.autoconfigure.sql.init.SqlR2dbcScriptDatabaseInitializer; SearchStrategy: all) did not find any beans (OnBeanCondition) + + DataSourceJmxConfiguration matched: + - @ConditionalOnProperty (spring.jmx.enabled=true) matched (OnPropertyCondition) + + DataSourceJmxConfiguration.Hikari matched: + - @ConditionalOnClass found required class 'com.zaxxer.hikari.HikariDataSource' (OnClassCondition) + - @ConditionalOnSingleCandidate (types: javax.sql.DataSource; SearchStrategy: all) found a single bean 'dataSource' (OnBeanCondition) + + DataSourcePoolMetadataProvidersConfiguration.HikariPoolDataSourceMetadataProviderConfiguration matched: + - @ConditionalOnClass found required class 'com.zaxxer.hikari.HikariDataSource' (OnClassCondition) + + DataSourceTransactionManagerAutoConfiguration matched: + - @ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.springframework.jdbc.core.JdbcTemplate', 'org.springframework.transaction.TransactionManager' (OnClassCondition) + + DataSourceTransactionManagerAutoConfiguration.JdbcTransactionManagerConfiguration matched: + - @ConditionalOnSingleCandidate (types: javax.sql.DataSource; SearchStrategy: all) found a single bean 'dataSource' (OnBeanCondition) + + DispatcherServletAutoConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition) + - found 'session' scope (OnWebApplicationCondition) + + DispatcherServletAutoConfiguration.DispatcherServletConfiguration matched: + - @ConditionalOnClass found required class 'jakarta.servlet.ServletRegistration' (OnClassCondition) + - Default DispatcherServlet did not find dispatcher servlet beans (DispatcherServletAutoConfiguration.DefaultDispatcherServletCondition) + + DispatcherServletAutoConfiguration.DispatcherServletRegistrationConfiguration matched: + - @ConditionalOnClass found required class 'jakarta.servlet.ServletRegistration' (OnClassCondition) + - DispatcherServlet Registration did not find servlet registration bean (DispatcherServletAutoConfiguration.DispatcherServletRegistrationCondition) + + DispatcherServletAutoConfiguration.DispatcherServletRegistrationConfiguration#dispatcherServletRegistration matched: + - @ConditionalOnBean (names: dispatcherServlet types: org.springframework.web.servlet.DispatcherServlet; SearchStrategy: all) found bean 'dispatcherServlet' (OnBeanCondition) + + EmbeddedWebServerFactoryCustomizerAutoConfiguration matched: + - @ConditionalOnWebApplication (required) found 'session' scope (OnWebApplicationCondition) + - @ConditionalOnWarDeployment the application is not deployed as a WAR file. (OnWarDeploymentCondition) + + EmbeddedWebServerFactoryCustomizerAutoConfiguration.TomcatWebServerFactoryCustomizerConfiguration matched: + - @ConditionalOnClass found required classes 'org.apache.catalina.startup.Tomcat', 'org.apache.coyote.UpgradeProtocol' (OnClassCondition) + + ErrorMvcAutoConfiguration matched: + - @ConditionalOnClass found required classes 'jakarta.servlet.Servlet', 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition) + - found 'session' scope (OnWebApplicationCondition) + + ErrorMvcAutoConfiguration#basicErrorController matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.web.servlet.error.ErrorController; SearchStrategy: current) did not find any beans (OnBeanCondition) + + ErrorMvcAutoConfiguration#errorAttributes matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.web.servlet.error.ErrorAttributes; SearchStrategy: current) did not find any beans (OnBeanCondition) + + ErrorMvcAutoConfiguration.DefaultErrorViewResolverConfiguration#conventionErrorViewResolver matched: + - @ConditionalOnBean (types: org.springframework.web.servlet.DispatcherServlet; SearchStrategy: all) found bean 'dispatcherServlet'; @ConditionalOnMissingBean (types: org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver; SearchStrategy: all) did not find any beans (OnBeanCondition) + + ErrorMvcAutoConfiguration.WhitelabelErrorViewConfiguration matched: + - @ConditionalOnProperty (server.error.whitelabel.enabled) matched (OnPropertyCondition) + - ErrorTemplate Missing did not find error template view (ErrorMvcAutoConfiguration.ErrorTemplateMissingCondition) + + ErrorMvcAutoConfiguration.WhitelabelErrorViewConfiguration#beanNameViewResolver matched: + - @ConditionalOnMissingBean (types: org.springframework.web.servlet.view.BeanNameViewResolver; SearchStrategy: all) did not find any beans (OnBeanCondition) + + ErrorMvcAutoConfiguration.WhitelabelErrorViewConfiguration#defaultErrorView matched: + - @ConditionalOnMissingBean (names: error; SearchStrategy: all) did not find any beans (OnBeanCondition) + + GenericCacheConfiguration matched: + - Cache org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration automatic cache type (CacheCondition) + + H2ConsoleAutoConfiguration matched: + - @ConditionalOnClass found required class 'org.h2.server.web.JakartaWebServlet' (OnClassCondition) + - found 'session' scope (OnWebApplicationCondition) + - @ConditionalOnProperty (spring.h2.console.enabled=true) matched (OnPropertyCondition) + + HibernateJpaAutoConfiguration matched: + - @ConditionalOnClass found required classes 'org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean', 'jakarta.persistence.EntityManager', 'org.hibernate.engine.spi.SessionImplementor' (OnClassCondition) + + HibernateJpaConfiguration matched: + - @ConditionalOnSingleCandidate (types: javax.sql.DataSource; SearchStrategy: all) found a single bean 'dataSource' (OnBeanCondition) + + HttpEncodingAutoConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.web.filter.CharacterEncodingFilter' (OnClassCondition) + - found 'session' scope (OnWebApplicationCondition) + - @ConditionalOnProperty (server.servlet.encoding.enabled) matched (OnPropertyCondition) + + HttpEncodingAutoConfiguration#characterEncodingFilter matched: + - @ConditionalOnMissingBean (types: org.springframework.web.filter.CharacterEncodingFilter; SearchStrategy: all) did not find any beans (OnBeanCondition) + + HttpMessageConvertersAutoConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.http.converter.HttpMessageConverter' (OnClassCondition) + - NoneNestedConditions 0 matched 1 did not; NestedCondition on HttpMessageConvertersAutoConfiguration.NotReactiveWebApplicationCondition.ReactiveWebApplication did not find reactive web application classes (HttpMessageConvertersAutoConfiguration.NotReactiveWebApplicationCondition) + + HttpMessageConvertersAutoConfiguration#messageConverters matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.autoconfigure.http.HttpMessageConverters; SearchStrategy: all) did not find any beans (OnBeanCondition) + + HttpMessageConvertersAutoConfiguration.StringHttpMessageConverterConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.http.converter.StringHttpMessageConverter' (OnClassCondition) + + HttpMessageConvertersAutoConfiguration.StringHttpMessageConverterConfiguration#stringHttpMessageConverter matched: + - @ConditionalOnMissingBean (types: org.springframework.http.converter.StringHttpMessageConverter; SearchStrategy: all) did not find any beans (OnBeanCondition) + + JacksonAutoConfiguration matched: + - @ConditionalOnClass found required class 'com.fasterxml.jackson.databind.ObjectMapper' (OnClassCondition) + + JacksonAutoConfiguration.Jackson2ObjectMapperBuilderCustomizerConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.http.converter.json.Jackson2ObjectMapperBuilder' (OnClassCondition) + + JacksonAutoConfiguration.JacksonObjectMapperBuilderConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.http.converter.json.Jackson2ObjectMapperBuilder' (OnClassCondition) + + JacksonAutoConfiguration.JacksonObjectMapperBuilderConfiguration#jacksonObjectMapperBuilder matched: + - @ConditionalOnMissingBean (types: org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; SearchStrategy: all) did not find any beans (OnBeanCondition) + + JacksonAutoConfiguration.JacksonObjectMapperConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.http.converter.json.Jackson2ObjectMapperBuilder' (OnClassCondition) + + JacksonAutoConfiguration.JacksonObjectMapperConfiguration#jacksonObjectMapper matched: + - @ConditionalOnMissingBean (types: com.fasterxml.jackson.databind.ObjectMapper; SearchStrategy: all) did not find any beans (OnBeanCondition) + + JacksonAutoConfiguration.ParameterNamesModuleConfiguration matched: + - @ConditionalOnClass found required class 'com.fasterxml.jackson.module.paramnames.ParameterNamesModule' (OnClassCondition) + + JacksonAutoConfiguration.ParameterNamesModuleConfiguration#parameterNamesModule matched: + - @ConditionalOnMissingBean (types: com.fasterxml.jackson.module.paramnames.ParameterNamesModule; SearchStrategy: all) did not find any beans (OnBeanCondition) + + JacksonHttpMessageConvertersConfiguration.MappingJackson2HttpMessageConverterConfiguration matched: + - @ConditionalOnClass found required class 'com.fasterxml.jackson.databind.ObjectMapper' (OnClassCondition) + - @ConditionalOnProperty (spring.mvc.converters.preferred-json-mapper=jackson) matched (OnPropertyCondition) + - @ConditionalOnBean (types: com.fasterxml.jackson.databind.ObjectMapper; SearchStrategy: all) found bean 'jacksonObjectMapper' (OnBeanCondition) + + JacksonHttpMessageConvertersConfiguration.MappingJackson2HttpMessageConverterConfiguration#mappingJackson2HttpMessageConverter matched: + - @ConditionalOnMissingBean (types: org.springframework.http.converter.json.MappingJackson2HttpMessageConverter ignored: org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter,org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter; SearchStrategy: all) did not find any beans (OnBeanCondition) + + JdbcClientAutoConfiguration matched: + - @ConditionalOnSingleCandidate (types: org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; SearchStrategy: all) found a single bean 'namedParameterJdbcTemplate'; @ConditionalOnMissingBean (types: org.springframework.jdbc.core.simple.JdbcClient; SearchStrategy: all) did not find any beans (OnBeanCondition) + + JdbcTemplateAutoConfiguration matched: + - @ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.springframework.jdbc.core.JdbcTemplate' (OnClassCondition) + - @ConditionalOnSingleCandidate (types: javax.sql.DataSource; SearchStrategy: all) found a single bean 'dataSource' (OnBeanCondition) + + JdbcTemplateConfiguration matched: + - @ConditionalOnMissingBean (types: org.springframework.jdbc.core.JdbcOperations; SearchStrategy: all) did not find any beans (OnBeanCondition) + + JpaBaseConfiguration#entityManagerFactory matched: + - @ConditionalOnMissingBean (types: org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean,jakarta.persistence.EntityManagerFactory; SearchStrategy: all) did not find any beans (OnBeanCondition) + + JpaBaseConfiguration#entityManagerFactoryBuilder matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; SearchStrategy: all) did not find any beans (OnBeanCondition) + + JpaBaseConfiguration#jpaVendorAdapter matched: + - @ConditionalOnMissingBean (types: org.springframework.orm.jpa.JpaVendorAdapter; SearchStrategy: all) did not find any beans (OnBeanCondition) + + JpaBaseConfiguration#transactionManager matched: + - @ConditionalOnMissingBean (types: org.springframework.transaction.TransactionManager; SearchStrategy: all) did not find any beans (OnBeanCondition) + + JpaBaseConfiguration.PersistenceManagedTypesConfiguration matched: + - @ConditionalOnMissingBean (types: org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean,jakarta.persistence.EntityManagerFactory; SearchStrategy: all) did not find any beans (OnBeanCondition) + + JpaBaseConfiguration.PersistenceManagedTypesConfiguration#persistenceManagedTypes matched: + - @ConditionalOnMissingBean (types: org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; SearchStrategy: all) did not find any beans (OnBeanCondition) + + JpaRepositoriesAutoConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.data.jpa.repository.JpaRepository' (OnClassCondition) + - @ConditionalOnProperty (spring.data.jpa.repositories.enabled=true) matched (OnPropertyCondition) + - @ConditionalOnBean (types: javax.sql.DataSource; SearchStrategy: all) found bean 'dataSource'; @ConditionalOnMissingBean (types: org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean,org.springframework.data.jpa.repository.config.JpaRepositoryConfigExtension; SearchStrategy: all) did not find any beans (OnBeanCondition) + + JtaAutoConfiguration matched: + - @ConditionalOnClass found required class 'jakarta.transaction.Transaction' (OnClassCondition) + - @ConditionalOnProperty (spring.jta.enabled) matched (OnPropertyCondition) + + LifecycleAutoConfiguration#defaultLifecycleProcessor matched: + - @ConditionalOnMissingBean (names: lifecycleProcessor; SearchStrategy: current) did not find any beans (OnBeanCondition) + + MultipartAutoConfiguration matched: + - @ConditionalOnClass found required classes 'jakarta.servlet.Servlet', 'org.springframework.web.multipart.support.StandardServletMultipartResolver', 'jakarta.servlet.MultipartConfigElement' (OnClassCondition) + - found 'session' scope (OnWebApplicationCondition) + - @ConditionalOnProperty (spring.servlet.multipart.enabled) matched (OnPropertyCondition) + + MultipartAutoConfiguration#multipartConfigElement matched: + - @ConditionalOnMissingBean (types: jakarta.servlet.MultipartConfigElement; SearchStrategy: all) did not find any beans (OnBeanCondition) + + MultipartAutoConfiguration#multipartResolver matched: + - @ConditionalOnMissingBean (types: org.springframework.web.multipart.MultipartResolver; SearchStrategy: all) did not find any beans (OnBeanCondition) + + NamedParameterJdbcTemplateConfiguration matched: + - @ConditionalOnSingleCandidate (types: org.springframework.jdbc.core.JdbcTemplate; SearchStrategy: all) found a single bean 'jdbcTemplate'; @ConditionalOnMissingBean (types: org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; SearchStrategy: all) did not find any beans (OnBeanCondition) + + NoOpCacheConfiguration matched: + - Cache org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration automatic cache type (CacheCondition) + + PersistenceExceptionTranslationAutoConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor' (OnClassCondition) + + PersistenceExceptionTranslationAutoConfiguration#persistenceExceptionTranslationPostProcessor matched: + - @ConditionalOnProperty (spring.dao.exceptiontranslation.enabled) matched (OnPropertyCondition) + - @ConditionalOnMissingBean (types: org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; SearchStrategy: all) did not find any beans (OnBeanCondition) + + PropertyPlaceholderAutoConfiguration#propertySourcesPlaceholderConfigurer matched: + - @ConditionalOnMissingBean (types: org.springframework.context.support.PropertySourcesPlaceholderConfigurer; SearchStrategy: current) did not find any beans (OnBeanCondition) + + RestClientAutoConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.web.client.RestClient' (OnClassCondition) + - NoneNestedConditions 0 matched 1 did not; NestedCondition on NotReactiveWebApplicationCondition.ReactiveWebApplication did not find reactive web application classes (NotReactiveWebApplicationCondition) + + RestClientAutoConfiguration#httpMessageConvertersRestClientCustomizer matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.autoconfigure.web.client.HttpMessageConvertersRestClientCustomizer; SearchStrategy: all) did not find any beans (OnBeanCondition) + + RestClientAutoConfiguration#restClientBuilder matched: + - @ConditionalOnMissingBean (types: org.springframework.web.client.RestClient$Builder; SearchStrategy: all) did not find any beans (OnBeanCondition) + + RestClientAutoConfiguration#restClientBuilderConfigurer matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.autoconfigure.web.client.RestClientBuilderConfigurer; SearchStrategy: all) did not find any beans (OnBeanCondition) + + RestClientAutoConfiguration#restClientSsl matched: + - @ConditionalOnBean (types: org.springframework.boot.ssl.SslBundles; SearchStrategy: all) found bean 'sslBundleRegistry'; @ConditionalOnMissingBean (types: org.springframework.boot.autoconfigure.web.client.RestClientSsl; SearchStrategy: all) did not find any beans (OnBeanCondition) + + RestTemplateAutoConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.web.client.RestTemplate' (OnClassCondition) + - NoneNestedConditions 0 matched 1 did not; NestedCondition on NotReactiveWebApplicationCondition.ReactiveWebApplication did not find reactive web application classes (NotReactiveWebApplicationCondition) + + RestTemplateAutoConfiguration#restTemplateBuilder matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.web.client.RestTemplateBuilder; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SecurityAutoConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.security.authentication.DefaultAuthenticationEventPublisher' (OnClassCondition) + + SecurityAutoConfiguration#authenticationEventPublisher matched: + - @ConditionalOnMissingBean (types: org.springframework.security.authentication.AuthenticationEventPublisher; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SecurityFilterAutoConfiguration matched: + - @ConditionalOnClass found required classes 'org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer', 'org.springframework.security.config.http.SessionCreationPolicy' (OnClassCondition) + - found 'session' scope (OnWebApplicationCondition) + + SecurityFilterAutoConfiguration#securityFilterChainRegistration matched: + - @ConditionalOnBean (names: springSecurityFilterChain; SearchStrategy: all) found bean 'springSecurityFilterChain' (OnBeanCondition) + + ServletWebServerFactoryAutoConfiguration matched: + - @ConditionalOnClass found required class 'jakarta.servlet.ServletRequest' (OnClassCondition) + - found 'session' scope (OnWebApplicationCondition) + + ServletWebServerFactoryAutoConfiguration#tomcatServletWebServerFactoryCustomizer matched: + - @ConditionalOnClass found required class 'org.apache.catalina.startup.Tomcat' (OnClassCondition) + + ServletWebServerFactoryConfiguration.EmbeddedTomcat matched: + - @ConditionalOnClass found required classes 'jakarta.servlet.Servlet', 'org.apache.catalina.startup.Tomcat', 'org.apache.coyote.UpgradeProtocol' (OnClassCondition) + - @ConditionalOnMissingBean (types: org.springframework.boot.web.servlet.server.ServletWebServerFactory; SearchStrategy: current) did not find any beans (OnBeanCondition) + + SimpleCacheConfiguration matched: + - Cache org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration automatic cache type (CacheCondition) + + SpringBootWebSecurityConfiguration matched: + - found 'session' scope (OnWebApplicationCondition) + + SpringDataWebAutoConfiguration matched: + - @ConditionalOnClass found required classes 'org.springframework.data.web.PageableHandlerMethodArgumentResolver', 'org.springframework.web.servlet.config.annotation.WebMvcConfigurer' (OnClassCondition) + - found 'session' scope (OnWebApplicationCondition) + - @ConditionalOnMissingBean (types: org.springframework.data.web.PageableHandlerMethodArgumentResolver; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDataWebAutoConfiguration#pageableCustomizer matched: + - @ConditionalOnMissingBean (types: org.springframework.data.web.config.PageableHandlerMethodArgumentResolverCustomizer; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDataWebAutoConfiguration#sortCustomizer matched: + - @ConditionalOnMissingBean (types: org.springframework.data.web.config.SortHandlerMethodArgumentResolverCustomizer; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocConfigProperties matched: + - @ConditionalOnProperty (springdoc.api-docs.enabled) matched (OnPropertyCondition) + - @ConditionalOnBean (types: org.springdoc.core.configuration.SpringDocConfiguration; SearchStrategy: all) found bean 'org.springdoc.core.configuration.SpringDocConfiguration' (OnBeanCondition) + + SpringDocConfiguration matched: + - @ConditionalOnWebApplication (required) found 'session' scope (OnWebApplicationCondition) + - @ConditionalOnProperty (springdoc.api-docs.enabled) matched (OnPropertyCondition) + + SpringDocConfiguration#fileSupportConverter matched: + - @ConditionalOnMissingBean (types: org.springdoc.core.converters.FileSupportConverter; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocConfiguration#openAPIBuilder matched: + - @ConditionalOnMissingBean (types: org.springdoc.core.service.OpenAPIService; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocConfiguration#operationBuilder matched: + - @ConditionalOnMissingBean (types: org.springdoc.core.service.OperationService; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocConfiguration#parameterBuilder matched: + - @ConditionalOnMissingBean (types: org.springdoc.core.service.GenericParameterService; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocConfiguration#polymorphicModelConverter matched: + - @ConditionalOnProperty (springdoc.model-converters.polymorphic-converter.enabled) matched (OnPropertyCondition) + - @ConditionalOnMissingBean (types: org.springdoc.core.converters.PolymorphicModelConverter; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocConfiguration#requestBodyBuilder matched: + - @ConditionalOnMissingBean (types: org.springdoc.core.service.RequestBodyService; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocConfiguration#responseSupportConverter matched: + - @ConditionalOnMissingBean (types: org.springdoc.core.converters.ResponseSupportConverter; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocConfiguration#schemaPropertyDeprecatingConverter matched: + - @ConditionalOnProperty (springdoc.model-converters.deprecating-converter.enabled) matched (OnPropertyCondition) + - @ConditionalOnMissingBean (types: org.springdoc.core.converters.SchemaPropertyDeprecatingConverter; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocConfiguration#securityParser matched: + - @ConditionalOnMissingBean (types: org.springdoc.core.service.SecurityService; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocConfiguration#springDocCustomizers matched: + - @ConditionalOnMissingBean (types: org.springdoc.core.customizers.SpringDocCustomizers; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocConfiguration#springDocProviders matched: + - @ConditionalOnMissingBean (types: org.springdoc.core.providers.SpringDocProviders; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocConfiguration#springdocObjectMapperProvider matched: + - @ConditionalOnMissingBean (types: org.springdoc.core.providers.ObjectMapperProvider; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocConfiguration.QuerydslProvider matched: + - @ConditionalOnClass found required class 'org.springframework.data.querydsl.binding.QuerydslBindingsFactory' (OnClassCondition) + + SpringDocConfiguration.QuerydslProvider#queryDslQuerydslPredicateOperationCustomizer matched: + - @ConditionalOnMissingBean (types: org.springdoc.core.customizers.QuerydslPredicateOperationCustomizer; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocConfiguration.SpringDocSpringDataWebPropertiesProvider matched: + - @ConditionalOnClass found required class 'org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties' (OnClassCondition) + + SpringDocConfiguration.SpringDocSpringDataWebPropertiesProvider#springDataWebPropertiesProvider matched: + - @ConditionalOnMissingBean (types: org.springdoc.core.providers.SpringDataWebPropertiesProvider; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocConfiguration.WebConversionServiceConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.boot.autoconfigure.web.format.WebConversionService' (OnClassCondition) + + SpringDocPageableConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.data.domain.Pageable' (OnClassCondition) + - @ConditionalOnWebApplication (required) found 'session' scope (OnWebApplicationCondition) + - @ConditionalOnProperty (springdoc.api-docs.enabled) matched (OnPropertyCondition) + - @ConditionalOnBean (types: org.springdoc.core.configuration.SpringDocConfiguration; SearchStrategy: all) found bean 'org.springdoc.core.configuration.SpringDocConfiguration' (OnBeanCondition) + + SpringDocPageableConfiguration#delegatingMethodParameterCustomizer matched: + - @ConditionalOnMissingBean (types: org.springdoc.core.customizers.DelegatingMethodParameterCustomizer; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocPageableConfiguration#pageOpenAPIConverter matched: + - @ConditionalOnClass found required classes 'org.springframework.data.web.PagedModel', 'org.springframework.data.web.config.SpringDataWebSettings' (OnClassCondition) + - @ConditionalOnMissingBean (types: org.springdoc.core.converters.PageOpenAPIConverter; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocPageableConfiguration#pageableOpenAPIConverter matched: + - @ConditionalOnProperty (springdoc.model-converters.pageable-converter.enabled) matched (OnPropertyCondition) + - @ConditionalOnMissingBean (types: org.springdoc.core.converters.PageableOpenAPIConverter; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocSecurityConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.security.web.SecurityFilterChain' (OnClassCondition) + - @ConditionalOnWebApplication (required) found 'session' scope (OnWebApplicationCondition) + - @ConditionalOnExpression (#{${springdoc.api-docs.enabled:true} and ${springdoc.enable-spring-security:true}}) resulted in true (OnExpressionCondition) + - @ConditionalOnBean (types: org.springdoc.core.configuration.SpringDocConfiguration; SearchStrategy: all) found bean 'org.springdoc.core.configuration.SpringDocConfiguration' (OnBeanCondition) + + SpringDocSecurityConfiguration.SpringSecurityLoginEndpointConfiguration matched: + - @ConditionalOnClass found required class 'jakarta.servlet.Filter' (OnClassCondition) + + SpringDocSortConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.data.domain.Sort' (OnClassCondition) + - @ConditionalOnWebApplication (required) found 'session' scope (OnWebApplicationCondition) + - @ConditionalOnProperty (springdoc.api-docs.enabled) matched (OnPropertyCondition) + - @ConditionalOnBean (types: org.springdoc.core.configuration.SpringDocConfiguration; SearchStrategy: all) found bean 'org.springdoc.core.configuration.SpringDocConfiguration' (OnBeanCondition) + + SpringDocSortConfiguration#sortOpenAPIConverter matched: + - @ConditionalOnProperty (springdoc.sort-converter.enabled) matched (OnPropertyCondition) + - @ConditionalOnMissingBean (types: org.springdoc.core.converters.SortOpenAPIConverter; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocUIConfiguration matched: + - @ConditionalOnWebApplication (required) found 'session' scope (OnWebApplicationCondition) + - @ConditionalOnBean (types: org.springdoc.core.configuration.SpringDocConfiguration; SearchStrategy: all) found bean 'org.springdoc.core.configuration.SpringDocConfiguration' (OnBeanCondition) + + SpringDocWebMvcConfiguration matched: + - found 'session' scope (OnWebApplicationCondition) + - @ConditionalOnProperty (springdoc.api-docs.enabled) matched (OnPropertyCondition) + - @ConditionalOnBean (types: org.springdoc.core.configuration.SpringDocConfiguration; SearchStrategy: all) found bean 'org.springdoc.core.configuration.SpringDocConfiguration' (OnBeanCondition) + + SpringDocWebMvcConfiguration#openApiResource matched: + - @ConditionalOnExpression (#{(${springdoc.use-management-port:false} == false ) and ${springdoc.enable-default-api-docs:true}}) resulted in true (OnExpressionCondition) + - @ConditionalOnMissingBean (types: org.springdoc.webmvc.api.OpenApiWebMvcResource; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocWebMvcConfiguration#requestBuilder matched: + - @ConditionalOnMissingBean (types: org.springdoc.webmvc.core.service.RequestService; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocWebMvcConfiguration#responseBuilder matched: + - @ConditionalOnMissingBean (types: org.springdoc.core.service.GenericResponseService; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocWebMvcConfiguration#springWebProvider matched: + - @ConditionalOnMissingBean (types: org.springdoc.core.providers.SpringWebProvider; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SpringDocWebMvcConfiguration.SpringDocWebMvcRouterConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.web.servlet.function.RouterFunction' (OnClassCondition) + + SpringDocWebMvcConfiguration.SpringDocWebMvcRouterConfiguration#routerFunctionProvider matched: + - @ConditionalOnMissingBean (types: org.springdoc.webmvc.core.providers.RouterFunctionWebMvcProvider; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SqlInitializationAutoConfiguration matched: + - @ConditionalOnProperty (spring.sql.init.enabled) matched (OnPropertyCondition) + - NoneNestedConditions 0 matched 1 did not; NestedCondition on SqlInitializationAutoConfiguration.SqlInitializationModeCondition.ModeIsNever @ConditionalOnProperty (spring.sql.init.mode=never) did not find property 'mode' (SqlInitializationAutoConfiguration.SqlInitializationModeCondition) + + SslAutoConfiguration#sslBundleRegistry matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.ssl.SslBundleRegistry,org.springframework.boot.ssl.SslBundles; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SwaggerConfig matched: + - found 'session' scope (OnWebApplicationCondition) + - @ConditionalOnProperty (springdoc.swagger-ui.enabled) matched (OnPropertyCondition) + - @ConditionalOnBean (types: org.springdoc.core.configuration.SpringDocConfiguration; SearchStrategy: all) found bean 'org.springdoc.core.configuration.SpringDocConfiguration' (OnBeanCondition) + + SwaggerConfig#indexPageTransformer matched: + - @ConditionalOnMissingBean (types: org.springdoc.webmvc.ui.SwaggerIndexTransformer; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SwaggerConfig#swaggerConfigResource matched: + - @ConditionalOnProperty (springdoc.use-management-port=false) matched (OnPropertyCondition) + - @ConditionalOnMissingBean (types: org.springdoc.webmvc.ui.SwaggerConfigResource; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SwaggerConfig#swaggerResourceResolver matched: + - @ConditionalOnMissingBean (types: org.springdoc.webmvc.ui.SwaggerResourceResolver; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SwaggerConfig#swaggerWebMvcConfigurer matched: + - @ConditionalOnMissingBean (types: org.springdoc.webmvc.ui.SwaggerWebMvcConfigurer; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SwaggerConfig#swaggerWelcome matched: + - @ConditionalOnProperty (springdoc.use-management-port=false) matched (OnPropertyCondition) + - @ConditionalOnMissingBean (types: org.springdoc.webmvc.ui.SwaggerWelcomeWebMvc; SearchStrategy: all) did not find any beans (OnBeanCondition) + + SwaggerUiConfigParameters matched: + - @ConditionalOnProperty (springdoc.swagger-ui.enabled) matched (OnPropertyCondition) + - @ConditionalOnBean (types: org.springdoc.core.configuration.SpringDocConfiguration; SearchStrategy: all) found bean 'org.springdoc.core.configuration.SpringDocConfiguration' (OnBeanCondition) + + SwaggerUiConfigProperties matched: + - @ConditionalOnProperty (springdoc.swagger-ui.enabled) matched (OnPropertyCondition) + - @ConditionalOnBean (types: org.springdoc.core.configuration.SpringDocConfiguration; SearchStrategy: all) found bean 'org.springdoc.core.configuration.SpringDocConfiguration' (OnBeanCondition) + + SwaggerUiOAuthProperties matched: + - @ConditionalOnProperty (springdoc.swagger-ui.enabled) matched (OnPropertyCondition) + - @ConditionalOnBean (types: org.springdoc.core.configuration.SpringDocConfiguration; SearchStrategy: all) found bean 'org.springdoc.core.configuration.SpringDocConfiguration' (OnBeanCondition) + + TaskExecutionAutoConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor' (OnClassCondition) + + TaskExecutorConfigurations.SimpleAsyncTaskExecutorBuilderConfiguration#simpleAsyncTaskExecutorBuilder matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder; SearchStrategy: all) did not find any beans (OnBeanCondition) + - @ConditionalOnThreading found PLATFORM (OnThreadingCondition) + + TaskExecutorConfigurations.TaskExecutorBuilderConfiguration#taskExecutorBuilder matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.task.TaskExecutorBuilder; SearchStrategy: all) did not find any beans (OnBeanCondition) + + TaskExecutorConfigurations.TaskExecutorConfiguration matched: + - @ConditionalOnMissingBean (types: java.util.concurrent.Executor; SearchStrategy: all) did not find any beans (OnBeanCondition) + + TaskExecutorConfigurations.TaskExecutorConfiguration#applicationTaskExecutor matched: + - @ConditionalOnThreading found PLATFORM (OnThreadingCondition) + + TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration#threadPoolTaskExecutorBuilder matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.task.TaskExecutorBuilder,org.springframework.boot.task.ThreadPoolTaskExecutorBuilder; SearchStrategy: all) did not find any beans (OnBeanCondition) + + TaskSchedulingAutoConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler' (OnClassCondition) + + TaskSchedulingConfigurations.SimpleAsyncTaskSchedulerBuilderConfiguration#simpleAsyncTaskSchedulerBuilder matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder; SearchStrategy: all) did not find any beans (OnBeanCondition) + - @ConditionalOnThreading found PLATFORM (OnThreadingCondition) + + TaskSchedulingConfigurations.TaskSchedulerBuilderConfiguration#taskSchedulerBuilder matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.task.TaskSchedulerBuilder; SearchStrategy: all) did not find any beans (OnBeanCondition) + + TaskSchedulingConfigurations.ThreadPoolTaskSchedulerBuilderConfiguration#threadPoolTaskSchedulerBuilder matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.task.TaskSchedulerBuilder,org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder; SearchStrategy: all) did not find any beans (OnBeanCondition) + + TransactionAutoConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.transaction.PlatformTransactionManager' (OnClassCondition) + + TransactionAutoConfiguration.EnableTransactionManagementConfiguration matched: + - @ConditionalOnBean (types: org.springframework.transaction.TransactionManager; SearchStrategy: all) found bean 'transactionManager'; @ConditionalOnMissingBean (types: org.springframework.transaction.annotation.AbstractTransactionManagementConfiguration; SearchStrategy: all) did not find any beans (OnBeanCondition) + + TransactionAutoConfiguration.EnableTransactionManagementConfiguration.CglibAutoProxyConfiguration matched: + - @ConditionalOnProperty (spring.aop.proxy-target-class=true) matched (OnPropertyCondition) + + TransactionAutoConfiguration.TransactionTemplateConfiguration matched: + - @ConditionalOnSingleCandidate (types: org.springframework.transaction.PlatformTransactionManager; SearchStrategy: all) found a single bean 'transactionManager' (OnBeanCondition) + + TransactionAutoConfiguration.TransactionTemplateConfiguration#transactionTemplate matched: + - @ConditionalOnMissingBean (types: org.springframework.transaction.support.TransactionOperations; SearchStrategy: all) did not find any beans (OnBeanCondition) + + TransactionManagerCustomizationAutoConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.transaction.PlatformTransactionManager' (OnClassCondition) + + TransactionManagerCustomizationAutoConfiguration#platformTransactionManagerCustomizers matched: + - @ConditionalOnMissingBean (types: org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; SearchStrategy: all) did not find any beans (OnBeanCondition) + + ValidationAutoConfiguration matched: + - @ConditionalOnClass found required class 'jakarta.validation.executable.ExecutableValidator' (OnClassCondition) + - @ConditionalOnResource found location classpath:META-INF/services/jakarta.validation.spi.ValidationProvider (OnResourceCondition) + + ValidationAutoConfiguration#defaultValidator matched: + - @ConditionalOnMissingBean (types: jakarta.validation.Validator; SearchStrategy: all) did not find any beans (OnBeanCondition) + + ValidationAutoConfiguration#methodValidationPostProcessor matched: + - @ConditionalOnMissingBean (types: org.springframework.validation.beanvalidation.MethodValidationPostProcessor; SearchStrategy: current) did not find any beans (OnBeanCondition) + + WebMvcAutoConfiguration matched: + - @ConditionalOnClass found required classes 'jakarta.servlet.Servlet', 'org.springframework.web.servlet.DispatcherServlet', 'org.springframework.web.servlet.config.annotation.WebMvcConfigurer' (OnClassCondition) + - found 'session' scope (OnWebApplicationCondition) + - @ConditionalOnMissingBean (types: org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; SearchStrategy: all) did not find any beans (OnBeanCondition) + + WebMvcAutoConfiguration#formContentFilter matched: + - @ConditionalOnProperty (spring.mvc.formcontent.filter.enabled) matched (OnPropertyCondition) + - @ConditionalOnMissingBean (types: org.springframework.web.filter.FormContentFilter; SearchStrategy: all) did not find any beans (OnBeanCondition) + + WebMvcAutoConfiguration.EnableWebMvcConfiguration#flashMapManager matched: + - @ConditionalOnMissingBean (names: flashMapManager; SearchStrategy: all) did not find any beans (OnBeanCondition) + + WebMvcAutoConfiguration.EnableWebMvcConfiguration#localeResolver matched: + - @ConditionalOnMissingBean (names: localeResolver; SearchStrategy: all) did not find any beans (OnBeanCondition) + + WebMvcAutoConfiguration.EnableWebMvcConfiguration#themeResolver matched: + - @ConditionalOnMissingBean (names: themeResolver; SearchStrategy: all) did not find any beans (OnBeanCondition) + + WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#defaultViewResolver matched: + - @ConditionalOnMissingBean (types: org.springframework.web.servlet.view.InternalResourceViewResolver; SearchStrategy: all) did not find any beans (OnBeanCondition) + + WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#requestContextFilter matched: + - @ConditionalOnMissingBean (types: org.springframework.web.context.request.RequestContextListener,org.springframework.web.filter.RequestContextFilter; SearchStrategy: all) did not find any beans (OnBeanCondition) + + WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#viewResolver matched: + - @ConditionalOnBean (types: org.springframework.web.servlet.ViewResolver; SearchStrategy: all) found beans 'defaultViewResolver', 'beanNameViewResolver', 'mvcViewResolver'; @ConditionalOnMissingBean (names: viewResolver types: org.springframework.web.servlet.view.ContentNegotiatingViewResolver; SearchStrategy: all) did not find any beans (OnBeanCondition) + + WebSocketServletAutoConfiguration matched: + - @ConditionalOnClass found required classes 'jakarta.servlet.Servlet', 'jakarta.websocket.server.ServerContainer' (OnClassCondition) + - found 'session' scope (OnWebApplicationCondition) + + WebSocketServletAutoConfiguration.TomcatWebSocketConfiguration matched: + - @ConditionalOnClass found required classes 'org.apache.catalina.startup.Tomcat', 'org.apache.tomcat.websocket.server.WsSci' (OnClassCondition) + + WebSocketServletAutoConfiguration.TomcatWebSocketConfiguration#websocketServletWebServerCustomizer matched: + - @ConditionalOnMissingBean (names: websocketServletWebServerCustomizer; SearchStrategy: all) did not find any beans (OnBeanCondition) + + +Negative matches: +----------------- + + ActiveMQAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'jakarta.jms.ConnectionFactory' (OnClassCondition) + + AopAutoConfiguration.AspectJAutoProxyingConfiguration.JdkDynamicAutoProxyConfiguration: + Did not match: + - @ConditionalOnProperty (spring.aop.proxy-target-class=false) did not find property 'proxy-target-class' (OnPropertyCondition) + + AopAutoConfiguration.ClassProxyingConfiguration: + Did not match: + - @ConditionalOnMissingClass found unwanted class 'org.aspectj.weaver.Advice' (OnClassCondition) + + ArtemisAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'jakarta.jms.ConnectionFactory' (OnClassCondition) + + BatchAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.batch.core.launch.JobLauncher' (OnClassCondition) + + Cache2kCacheConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.cache2k.Cache2kBuilder' (OnClassCondition) + + CacheAutoConfiguration: + Did not match: + - @ConditionalOnBean (types: org.springframework.cache.interceptor.CacheAspectSupport; SearchStrategy: all) did not find any beans of type org.springframework.cache.interceptor.CacheAspectSupport (OnBeanCondition) + Matched: + - @ConditionalOnClass found required class 'org.springframework.cache.CacheManager' (OnClassCondition) + + CacheAutoConfiguration.CacheManagerEntityManagerFactoryDependsOnPostProcessor: + Did not match: + - Ancestor org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration did not match (ConditionEvaluationReport.AncestorsMatchedCondition) + Matched: + - @ConditionalOnClass found required class 'org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean' (OnClassCondition) + + CaffeineCacheConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.github.benmanes.caffeine.cache.Caffeine' (OnClassCondition) + + CassandraAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.datastax.oss.driver.api.core.CqlSession' (OnClassCondition) + + CassandraDataAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.datastax.oss.driver.api.core.CqlSession' (OnClassCondition) + + CassandraReactiveDataAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.datastax.oss.driver.api.core.CqlSession' (OnClassCondition) + + CassandraReactiveRepositoriesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.data.cassandra.ReactiveSession' (OnClassCondition) + + CassandraRepositoriesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.datastax.oss.driver.api.core.CqlSession' (OnClassCondition) + + ClientHttpConnectorAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.web.reactive.function.client.WebClient' (OnClassCondition) + + CodecsAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.web.reactive.function.client.WebClient' (OnClassCondition) + + CouchbaseAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.couchbase.client.java.Cluster' (OnClassCondition) + + CouchbaseCacheConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.couchbase.client.java.Cluster' (OnClassCondition) + + CouchbaseDataAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.couchbase.client.java.Bucket' (OnClassCondition) + + CouchbaseReactiveDataAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.couchbase.client.java.Cluster' (OnClassCondition) + + CouchbaseReactiveRepositoriesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.couchbase.client.java.Cluster' (OnClassCondition) + + CouchbaseRepositoriesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.couchbase.client.java.Bucket' (OnClassCondition) + + DataSourceAutoConfiguration.EmbeddedDatabaseConfiguration: + Did not match: + - EmbeddedDataSource spring.datasource.url is set (DataSourceAutoConfiguration.EmbeddedDatabaseCondition) + + DataSourceCheckpointRestoreConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.crac.Resource' (OnClassCondition) + + DataSourceConfiguration.Dbcp2: + Did not match: + - @ConditionalOnClass did not find required class 'org.apache.commons.dbcp2.BasicDataSource' (OnClassCondition) + + DataSourceConfiguration.Generic: + Did not match: + - @ConditionalOnProperty (spring.datasource.type) did not find property 'spring.datasource.type' (OnPropertyCondition) + + DataSourceConfiguration.OracleUcp: + Did not match: + - @ConditionalOnClass did not find required classes 'oracle.ucp.jdbc.PoolDataSourceImpl', 'oracle.jdbc.OracleConnection' (OnClassCondition) + + DataSourceConfiguration.Tomcat: + Did not match: + - @ConditionalOnClass did not find required class 'org.apache.tomcat.jdbc.pool.DataSource' (OnClassCondition) + + DataSourceJmxConfiguration.TomcatDataSourceJmxConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.apache.tomcat.jdbc.pool.DataSourceProxy' (OnClassCondition) + + DataSourcePoolMetadataProvidersConfiguration.CommonsDbcp2PoolDataSourceMetadataProviderConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.apache.commons.dbcp2.BasicDataSource' (OnClassCondition) + + DataSourcePoolMetadataProvidersConfiguration.OracleUcpPoolDataSourceMetadataProviderConfiguration: + Did not match: + - @ConditionalOnClass did not find required classes 'oracle.ucp.jdbc.PoolDataSource', 'oracle.jdbc.OracleConnection' (OnClassCondition) + + DataSourcePoolMetadataProvidersConfiguration.TomcatDataSourcePoolMetadataProviderConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.apache.tomcat.jdbc.pool.DataSource' (OnClassCondition) + + DataSourceTransactionManagerAutoConfiguration.JdbcTransactionManagerConfiguration#transactionManager: + Did not match: + - @ConditionalOnMissingBean (types: org.springframework.transaction.TransactionManager; SearchStrategy: all) found beans of type 'org.springframework.transaction.TransactionManager' transactionManager (OnBeanCondition) + + DispatcherServletAutoConfiguration.DispatcherServletConfiguration#multipartResolver: + Did not match: + - @ConditionalOnBean (types: org.springframework.web.multipart.MultipartResolver; SearchStrategy: all) did not find any beans of type org.springframework.web.multipart.MultipartResolver (OnBeanCondition) + + ElasticsearchClientAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'co.elastic.clients.elasticsearch.ElasticsearchClient' (OnClassCondition) + + ElasticsearchDataAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate' (OnClassCondition) + + ElasticsearchRepositoriesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.data.elasticsearch.repository.ElasticsearchRepository' (OnClassCondition) + + ElasticsearchRestClientAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.elasticsearch.client.RestClientBuilder' (OnClassCondition) + + EmbeddedLdapAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.unboundid.ldap.listener.InMemoryDirectoryServer' (OnClassCondition) + + EmbeddedWebServerFactoryCustomizerAutoConfiguration.JettyWebServerFactoryCustomizerConfiguration: + Did not match: + - @ConditionalOnClass did not find required classes 'org.eclipse.jetty.server.Server', 'org.eclipse.jetty.util.Loader', 'org.eclipse.jetty.ee10.webapp.WebAppContext' (OnClassCondition) + + EmbeddedWebServerFactoryCustomizerAutoConfiguration.NettyWebServerFactoryCustomizerConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'reactor.netty.http.server.HttpServer' (OnClassCondition) + + EmbeddedWebServerFactoryCustomizerAutoConfiguration.TomcatWebServerFactoryCustomizerConfiguration#tomcatVirtualThreadsProtocolHandlerCustomizer: + Did not match: + - @ConditionalOnThreading did not find VIRTUAL (OnThreadingCondition) + + EmbeddedWebServerFactoryCustomizerAutoConfiguration.UndertowWebServerFactoryCustomizerConfiguration: + Did not match: + - @ConditionalOnClass did not find required classes 'io.undertow.Undertow', 'org.xnio.SslClientAuthMode' (OnClassCondition) + + ErrorWebFluxAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.web.reactive.config.WebFluxConfigurer' (OnClassCondition) + + FlywayAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.flywaydb.core.Flyway' (OnClassCondition) + + FreeMarkerAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'freemarker.template.Configuration' (OnClassCondition) + + GraphQlAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'graphql.GraphQL' (OnClassCondition) + + GraphQlQueryByExampleAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'graphql.GraphQL' (OnClassCondition) + + GraphQlQuerydslAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.querydsl.core.Query' (OnClassCondition) + + GraphQlRSocketAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'graphql.GraphQL' (OnClassCondition) + + GraphQlReactiveQueryByExampleAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'graphql.GraphQL' (OnClassCondition) + + GraphQlReactiveQuerydslAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.querydsl.core.Query' (OnClassCondition) + + GraphQlWebFluxAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'graphql.GraphQL' (OnClassCondition) + + GraphQlWebFluxSecurityAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'graphql.GraphQL' (OnClassCondition) + + GraphQlWebMvcAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'graphql.GraphQL' (OnClassCondition) + + GraphQlWebMvcSecurityAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'graphql.GraphQL' (OnClassCondition) + + GroovyTemplateAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'groovy.text.markup.MarkupTemplateEngine' (OnClassCondition) + + GsonAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.google.gson.Gson' (OnClassCondition) + + GsonHttpMessageConvertersConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.google.gson.Gson' (OnClassCondition) + + HazelcastAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.hazelcast.core.HazelcastInstance' (OnClassCondition) + + HazelcastCacheConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.hazelcast.core.HazelcastInstance' (OnClassCondition) + + HazelcastJpaDependencyAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.hazelcast.core.HazelcastInstance' (OnClassCondition) + + HttpHandlerAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.web.reactive.DispatcherHandler' (OnClassCondition) + + HypermediaAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.hateoas.EntityModel' (OnClassCondition) + + InfinispanCacheConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.infinispan.spring.embedded.provider.SpringEmbeddedCacheManager' (OnClassCondition) + + InfluxDbAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.influxdb.InfluxDB' (OnClassCondition) + + IntegrationAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.integration.config.EnableIntegration' (OnClassCondition) + + JCacheCacheConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'javax.cache.Caching' (OnClassCondition) + + JacksonHttpMessageConvertersConfiguration.MappingJackson2XmlHttpMessageConverterConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.fasterxml.jackson.dataformat.xml.XmlMapper' (OnClassCondition) + + JdbcRepositoriesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration' (OnClassCondition) + + JerseyAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.glassfish.jersey.server.spring.SpringComponentProvider' (OnClassCondition) + + JmsAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'jakarta.jms.Message' (OnClassCondition) + + JmxAutoConfiguration: + Did not match: + - @ConditionalOnProperty (spring.jmx.enabled=true) did not find property 'enabled' (OnPropertyCondition) + Matched: + - @ConditionalOnClass found required class 'org.springframework.jmx.export.MBeanExporter' (OnClassCondition) + + JndiConnectionFactoryAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.jms.core.JmsTemplate' (OnClassCondition) + + JndiDataSourceAutoConfiguration: + Did not match: + - @ConditionalOnProperty (spring.datasource.jndi-name) did not find property 'jndi-name' (OnPropertyCondition) + Matched: + - @ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType' (OnClassCondition) + + JndiJtaConfiguration: + Did not match: + - @ConditionalOnJndi JNDI environment is not available (OnJndiCondition) + Matched: + - @ConditionalOnClass found required class 'org.springframework.transaction.jta.JtaTransactionManager' (OnClassCondition) + + JooqAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.jooq.DSLContext' (OnClassCondition) + + JpaBaseConfiguration.JpaWebConfiguration: + Did not match: + - @ConditionalOnProperty (spring.jpa.open-in-view=true) found different value in property 'open-in-view' (OnPropertyCondition) + Matched: + - @ConditionalOnClass found required class 'org.springframework.web.servlet.config.annotation.WebMvcConfigurer' (OnClassCondition) + - found 'session' scope (OnWebApplicationCondition) + + JpaRepositoriesAutoConfiguration#entityManagerFactoryBootstrapExecutorCustomizer: + Did not match: + - AnyNestedCondition 0 matched 2 did not; NestedCondition on JpaRepositoriesAutoConfiguration.BootstrapExecutorCondition.LazyBootstrapMode @ConditionalOnProperty (spring.data.jpa.repositories.bootstrap-mode=lazy) did not find property 'bootstrap-mode'; NestedCondition on JpaRepositoriesAutoConfiguration.BootstrapExecutorCondition.DeferredBootstrapMode @ConditionalOnProperty (spring.data.jpa.repositories.bootstrap-mode=deferred) did not find property 'bootstrap-mode' (JpaRepositoriesAutoConfiguration.BootstrapExecutorCondition) + + JsonbAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'jakarta.json.bind.Jsonb' (OnClassCondition) + + JsonbHttpMessageConvertersConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'jakarta.json.bind.Jsonb' (OnClassCondition) + + KafkaAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.kafka.core.KafkaTemplate' (OnClassCondition) + + LdapAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.ldap.core.ContextSource' (OnClassCondition) + + LdapRepositoriesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.data.ldap.repository.LdapRepository' (OnClassCondition) + + LiquibaseAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'liquibase.change.DatabaseChange' (OnClassCondition) + + MailSenderAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'jakarta.mail.internet.MimeMessage' (OnClassCondition) + + MailSenderValidatorAutoConfiguration: + Did not match: + - @ConditionalOnSingleCandidate did not find required type 'org.springframework.mail.javamail.JavaMailSenderImpl' (OnBeanCondition) + + MessageSourceAutoConfiguration: + Did not match: + - ResourceBundle did not find bundle with basename messages (MessageSourceAutoConfiguration.ResourceBundleCondition) + + MongoAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.mongodb.client.MongoClient' (OnClassCondition) + + MongoDataAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.mongodb.client.MongoClient' (OnClassCondition) + + MongoReactiveAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.mongodb.reactivestreams.client.MongoClient' (OnClassCondition) + + MongoReactiveDataAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.mongodb.reactivestreams.client.MongoClient' (OnClassCondition) + + MongoReactiveRepositoriesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.mongodb.reactivestreams.client.MongoClient' (OnClassCondition) + + MongoRepositoriesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.mongodb.client.MongoClient' (OnClassCondition) + + MultipleOpenApiSupportConfiguration: + Did not match: + - AnyNestedCondition 0 matched 2 did not; NestedCondition on MultipleOpenApiSupportCondition.OnActuatorDifferentPort @ConditionalOnProperty (springdoc.show-actuator) did not find property 'springdoc.show-actuator'; NestedCondition on MultipleOpenApiSupportCondition.OnMultipleOpenApiSupportCondition AnyNestedCondition 0 matched 3 did not; NestedCondition on MultipleOpenApiGroupsCondition.OnListGroupedOpenApiBean @ConditionalOnBean (types: org.springdoc.core.models.GroupedOpenApi; SearchStrategy: all) did not find any beans of type org.springdoc.core.models.GroupedOpenApi; NestedCondition on MultipleOpenApiGroupsCondition.OnGroupConfigProperty @ConditionalOnProperty (springdoc.group-configs[0].group) did not find property 'springdoc.group-configs[0].group'; NestedCondition on MultipleOpenApiGroupsCondition.OnGroupedOpenApiBean @ConditionalOnBean (types: org.springdoc.core.models.GroupedOpenApi; SearchStrategy: all) did not find any beans of type org.springdoc.core.models.GroupedOpenApi (MultipleOpenApiSupportCondition) + Matched: + - found 'session' scope (OnWebApplicationCondition) + - @ConditionalOnProperty (springdoc.api-docs.enabled) matched (OnPropertyCondition) + + MultipleOpenApiSupportConfiguration.SpringDocWebMvcActuatorDifferentConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping' (OnClassCondition) + - Ancestor org.springdoc.webmvc.core.configuration.MultipleOpenApiSupportConfiguration did not match (ConditionEvaluationReport.AncestorsMatchedCondition) + + MustacheAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.samskivert.mustache.Mustache' (OnClassCondition) + + Neo4jAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.neo4j.driver.Driver' (OnClassCondition) + + Neo4jDataAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.neo4j.driver.Driver' (OnClassCondition) + + Neo4jReactiveDataAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.neo4j.driver.Driver' (OnClassCondition) + + Neo4jReactiveRepositoriesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.neo4j.driver.Driver' (OnClassCondition) + + Neo4jRepositoriesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.neo4j.driver.Driver' (OnClassCondition) + + NettyAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'io.netty.util.NettyRuntime' (OnClassCondition) + + OAuth2AuthorizationServerAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.security.oauth2.server.authorization.OAuth2Authorization' (OnClassCondition) + + OAuth2AuthorizationServerJwtAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.security.oauth2.server.authorization.OAuth2Authorization' (OnClassCondition) + + OAuth2ClientAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.security.oauth2.client.registration.ClientRegistration' (OnClassCondition) + + OAuth2ResourceServerAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken' (OnClassCondition) + + ProjectInfoAutoConfiguration#buildProperties: + Did not match: + - @ConditionalOnResource did not find resource '${spring.info.build.location:classpath:META-INF/build-info.properties}' (OnResourceCondition) + + ProjectInfoAutoConfiguration#gitProperties: + Did not match: + - GitResource did not find git info at classpath:git.properties (ProjectInfoAutoConfiguration.GitResourceAvailableCondition) + + PulsarAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.apache.pulsar.client.api.PulsarClient' (OnClassCondition) + + PulsarReactiveAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.apache.pulsar.client.api.PulsarClient' (OnClassCondition) + + QuartzAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.quartz.Scheduler' (OnClassCondition) + + R2dbcAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'io.r2dbc.spi.ConnectionFactory' (OnClassCondition) + + R2dbcDataAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.data.r2dbc.core.R2dbcEntityTemplate' (OnClassCondition) + + R2dbcInitializationConfiguration: + Did not match: + - @ConditionalOnClass did not find required classes 'io.r2dbc.spi.ConnectionFactory', 'org.springframework.r2dbc.connection.init.DatabasePopulator' (OnClassCondition) + + R2dbcRepositoriesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'io.r2dbc.spi.ConnectionFactory' (OnClassCondition) + + R2dbcTransactionManagerAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.r2dbc.connection.R2dbcTransactionManager' (OnClassCondition) + + RSocketGraphQlClientAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'graphql.GraphQL' (OnClassCondition) + + RSocketMessagingAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'io.rsocket.RSocket' (OnClassCondition) + + RSocketRequesterAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'io.rsocket.RSocket' (OnClassCondition) + + RSocketSecurityAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor' (OnClassCondition) + + RSocketServerAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'io.rsocket.core.RSocketServer' (OnClassCondition) + + RSocketStrategiesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'io.netty.buffer.PooledByteBufAllocator' (OnClassCondition) + + RabbitAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.rabbitmq.client.Channel' (OnClassCondition) + + ReactiveElasticsearchClientAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'co.elastic.clients.transport.ElasticsearchTransport' (OnClassCondition) + + ReactiveElasticsearchRepositoriesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'reactor.core.publisher.Mono' (OnClassCondition) + + ReactiveMultipartAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.web.reactive.config.WebFluxConfigurer' (OnClassCondition) + + ReactiveOAuth2ClientAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'reactor.core.publisher.Flux' (OnClassCondition) + + ReactiveOAuth2ResourceServerAutoConfiguration: + Did not match: + - @ConditionalOnWebApplication did not find reactive web application classes (OnWebApplicationCondition) + + ReactiveSecurityAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'reactor.core.publisher.Flux' (OnClassCondition) + + ReactiveUserDetailsServiceAutoConfiguration: + Did not match: + - AnyNestedCondition 0 matched 2 did not; NestedCondition on ReactiveUserDetailsServiceAutoConfiguration.RSocketEnabledOrReactiveWebApplication.ReactiveWebApplicationCondition did not find reactive web application classes; NestedCondition on ReactiveUserDetailsServiceAutoConfiguration.RSocketEnabledOrReactiveWebApplication.RSocketSecurityEnabledCondition @ConditionalOnBean (types: org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; SearchStrategy: all) did not find any beans of type org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler (ReactiveUserDetailsServiceAutoConfiguration.RSocketEnabledOrReactiveWebApplication) + Matched: + - @ConditionalOnClass found required class 'org.springframework.security.authentication.ReactiveAuthenticationManager' (OnClassCondition) + - AnyNestedCondition 1 matched 2 did not; NestedCondition on ReactiveUserDetailsServiceAutoConfiguration.MissingAlternativeOrUserPropertiesConfigured.PasswordConfigured @ConditionalOnProperty (spring.security.user.password) did not find property 'password'; NestedCondition on ReactiveUserDetailsServiceAutoConfiguration.MissingAlternativeOrUserPropertiesConfigured.NameConfigured @ConditionalOnProperty (spring.security.user.name) did not find property 'name'; NestedCondition on ReactiveUserDetailsServiceAutoConfiguration.MissingAlternativeOrUserPropertiesConfigured.MissingAlternative @ConditionalOnMissingClass did not find unwanted classes 'org.springframework.security.oauth2.client.registration.ClientRegistrationRepository', 'org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector' (ReactiveUserDetailsServiceAutoConfiguration.MissingAlternativeOrUserPropertiesConfigured) + + ReactiveWebServerFactoryAutoConfiguration: + Did not match: + - @ConditionalOnWebApplication did not find reactive web application classes (OnWebApplicationCondition) + + ReactorAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'reactor.core.publisher.Hooks' (OnClassCondition) + + RedisAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.data.redis.core.RedisOperations' (OnClassCondition) + + RedisCacheConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.data.redis.connection.RedisConnectionFactory' (OnClassCondition) + + RedisReactiveAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'reactor.core.publisher.Flux' (OnClassCondition) + + RedisRepositoriesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.data.redis.repository.configuration.EnableRedisRepositories' (OnClassCondition) + + RepositoryRestMvcAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration' (OnClassCondition) + + Saml2RelyingPartyAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository' (OnClassCondition) + + SecurityDataConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.security.data.repository.query.SecurityEvaluationContextExtension' (OnClassCondition) + + SendGridAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.sendgrid.SendGrid' (OnClassCondition) + + ServletWebServerFactoryAutoConfiguration.ForwardedHeaderFilterConfiguration: + Did not match: + - @ConditionalOnProperty (server.forward-headers-strategy=framework) did not find property 'server.forward-headers-strategy' (OnPropertyCondition) + + ServletWebServerFactoryConfiguration.EmbeddedJetty: + Did not match: + - @ConditionalOnClass did not find required classes 'org.eclipse.jetty.server.Server', 'org.eclipse.jetty.util.Loader', 'org.eclipse.jetty.ee10.webapp.WebAppContext' (OnClassCondition) + + ServletWebServerFactoryConfiguration.EmbeddedUndertow: + Did not match: + - @ConditionalOnClass did not find required classes 'io.undertow.Undertow', 'org.xnio.SslClientAuthMode' (OnClassCondition) + + SessionAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.session.Session' (OnClassCondition) + + SpringApplicationAdminJmxAutoConfiguration: + Did not match: + - @ConditionalOnProperty (spring.application.admin.enabled=true) did not find property 'enabled' (OnPropertyCondition) + + SpringBootWebSecurityConfiguration.SecurityFilterChainConfiguration: + Did not match: + - AllNestedConditions 1 matched 1 did not; NestedCondition on DefaultWebSecurityCondition.Beans @ConditionalOnMissingBean (types: org.springframework.security.web.SecurityFilterChain; SearchStrategy: all) found beans of type 'org.springframework.security.web.SecurityFilterChain' securityFilterChain; NestedCondition on DefaultWebSecurityCondition.Classes @ConditionalOnClass found required classes 'org.springframework.security.web.SecurityFilterChain', 'org.springframework.security.config.annotation.web.builders.HttpSecurity' (DefaultWebSecurityCondition) + + SpringBootWebSecurityConfiguration.WebSecurityEnablerConfiguration: + Did not match: + - @ConditionalOnMissingBean (names: springSecurityFilterChain; SearchStrategy: all) found beans named springSecurityFilterChain (OnBeanCondition) + Matched: + - @ConditionalOnClass found required class 'org.springframework.security.config.annotation.web.configuration.EnableWebSecurity' (OnClassCondition) + + SpringDocConfiguration#propertiesResolverForSchema: + Did not match: + - @ConditionalOnProperty (springdoc.api-docs.resolve-schema-properties) did not find property 'springdoc.api-docs.resolve-schema-properties' (OnPropertyCondition) + + SpringDocConfiguration#propertyCustomizingConverter: + Did not match: + - @ConditionalOnBean (types: org.springdoc.core.customizers.PropertyCustomizer; SearchStrategy: all) did not find any beans of type org.springdoc.core.customizers.PropertyCustomizer (OnBeanCondition) + + SpringDocConfiguration#springdocBeanFactoryPostProcessor: + Did not match: + - AnyNestedCondition 0 matched 2 did not; NestedCondition on CacheOrGroupedOpenApiCondition.OnCacheDisabled found non-matching nested conditions @ConditionalOnProperty (springdoc.cache.disabled) did not find property 'springdoc.cache.disabled'; NestedCondition on CacheOrGroupedOpenApiCondition.OnMultipleOpenApiSupportCondition AnyNestedCondition 0 matched 2 did not; NestedCondition on MultipleOpenApiSupportCondition.OnActuatorDifferentPort @ConditionalOnProperty (springdoc.show-actuator) did not find property 'springdoc.show-actuator'; NestedCondition on MultipleOpenApiSupportCondition.OnMultipleOpenApiSupportCondition AnyNestedCondition 0 matched 3 did not; NestedCondition on MultipleOpenApiGroupsCondition.OnListGroupedOpenApiBean @ConditionalOnBean (types: org.springdoc.core.models.GroupedOpenApi; SearchStrategy: all) did not find any beans of type org.springdoc.core.models.GroupedOpenApi; NestedCondition on MultipleOpenApiGroupsCondition.OnGroupConfigProperty @ConditionalOnProperty (springdoc.group-configs[0].group) did not find property 'springdoc.group-configs[0].group'; NestedCondition on MultipleOpenApiGroupsCondition.OnGroupedOpenApiBean @ConditionalOnBean (types: org.springdoc.core.models.GroupedOpenApi; SearchStrategy: all) did not find any beans of type org.springdoc.core.models.GroupedOpenApi (CacheOrGroupedOpenApiCondition) + Matched: + - @ConditionalOnClass found required class 'org.springframework.boot.context.properties.bind.BindResult' (OnClassCondition) + + SpringDocConfiguration#springdocBeanFactoryPostProcessor2: + Did not match: + - @ConditionalOnMissingClass found unwanted class 'org.springframework.boot.context.properties.bind.BindResult' (OnClassCondition) + + SpringDocConfiguration.SpringDocActuatorConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties' (OnClassCondition) + + SpringDocConfiguration.SpringDocRepositoryRestConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.data.rest.core.config.RepositoryRestConfiguration' (OnClassCondition) + + SpringDocConfiguration.SpringDocWebFluxSupportConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'reactor.core.publisher.Flux' (OnClassCondition) + + SpringDocDataRestConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.data.rest.core.config.RepositoryRestConfiguration' (OnClassCondition) + + SpringDocFunctionCatalogConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.cloud.function.web.function.FunctionEndpointInitializer' (OnClassCondition) + + SpringDocGroovyConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'groovy.lang.MetaClass' (OnClassCondition) + + SpringDocHateoasConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.hateoas.server.LinkRelationProvider' (OnClassCondition) + + SpringDocJacksonKotlinModuleConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.fasterxml.jackson.module.kotlin.KotlinModule' (OnClassCondition) + + SpringDocJavadocConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'com.github.therapi.runtimejavadoc.CommentFormatter' (OnClassCondition) + + SpringDocKotlinConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'kotlin.coroutines.Continuation' (OnClassCondition) + + SpringDocKotlinxConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'kotlinx.coroutines.flow.Flow' (OnClassCondition) + + SpringDocSecurityConfiguration.SpringDocSecurityOAuth2ClientConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient' (OnClassCondition) + + SpringDocSecurityConfiguration.SpringDocSecurityOAuth2Configuration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService' (OnClassCondition) + + SpringDocSecurityConfiguration.SpringSecurityLoginEndpointConfiguration#springSecurityLoginEndpointCustomiser: + Did not match: + - @ConditionalOnProperty (springdoc.show-login-endpoint) did not find property 'springdoc.show-login-endpoint' (OnPropertyCondition) + + SpringDocSortConfiguration#delegatingMethodParameterCustomizer: + Did not match: + - @ConditionalOnMissingBean (types: org.springdoc.core.customizers.DelegatingMethodParameterCustomizer; SearchStrategy: all) found beans of type 'org.springdoc.core.customizers.DelegatingMethodParameterCustomizer' delegatingMethodParameterCustomizer (OnBeanCondition) + + SpringDocWebMvcConfiguration.SpringDocWebMvcActuatorConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping' (OnClassCondition) + + SwaggerConfig#springWebProvider: + Did not match: + - @ConditionalOnMissingBean (types: org.springdoc.core.providers.SpringWebProvider; SearchStrategy: all) found beans of type 'org.springdoc.core.providers.SpringWebProvider' springWebProvider (OnBeanCondition) + + SwaggerConfig#swaggerUiConfigParameters: + Did not match: + - @ConditionalOnMissingBean (types: org.springdoc.core.properties.SwaggerUiConfigParameters; SearchStrategy: all) found beans of type 'org.springdoc.core.properties.SwaggerUiConfigParameters' org.springdoc.core.properties.SwaggerUiConfigParameters (OnBeanCondition) + + SwaggerConfig#swaggerUiHome: + Did not match: + - @ConditionalOnProperty (springdoc.swagger-ui.use-root-path=true) did not find property 'springdoc.swagger-ui.use-root-path' (OnPropertyCondition) + + SwaggerConfig.SwaggerActuatorWelcomeConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping' (OnClassCondition) + + TaskExecutorConfigurations.SimpleAsyncTaskExecutorBuilderConfiguration#simpleAsyncTaskExecutorBuilderVirtualThreads: + Did not match: + - @ConditionalOnMissingBean (types: org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder; SearchStrategy: all) found beans of type 'org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder' simpleAsyncTaskExecutorBuilder (OnBeanCondition) + + TaskExecutorConfigurations.TaskExecutorConfiguration#applicationTaskExecutorVirtualThreads: + Did not match: + - @ConditionalOnThreading did not find VIRTUAL (OnThreadingCondition) + + TaskSchedulingAutoConfiguration#scheduledBeanLazyInitializationExcludeFilter: + Did not match: + - @ConditionalOnBean (names: org.springframework.context.annotation.internalScheduledAnnotationProcessor; SearchStrategy: all) did not find any beans named org.springframework.context.annotation.internalScheduledAnnotationProcessor (OnBeanCondition) + + TaskSchedulingConfigurations.SimpleAsyncTaskSchedulerBuilderConfiguration#simpleAsyncTaskSchedulerBuilderVirtualThreads: + Did not match: + - @ConditionalOnMissingBean (types: org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder; SearchStrategy: all) found beans of type 'org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder' simpleAsyncTaskSchedulerBuilder (OnBeanCondition) + + TaskSchedulingConfigurations.TaskSchedulerConfiguration: + Did not match: + - @ConditionalOnBean (names: org.springframework.context.annotation.internalScheduledAnnotationProcessor; SearchStrategy: all) did not find any beans named org.springframework.context.annotation.internalScheduledAnnotationProcessor (OnBeanCondition) + + ThymeleafAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.thymeleaf.spring6.SpringTemplateEngine' (OnClassCondition) + + TransactionAutoConfiguration#transactionalOperator: + Did not match: + - @ConditionalOnSingleCandidate (types: org.springframework.transaction.ReactiveTransactionManager; SearchStrategy: all) did not find any beans (OnBeanCondition) + + TransactionAutoConfiguration.AspectJTransactionManagementConfiguration: + Did not match: + - @ConditionalOnBean (types: org.springframework.transaction.aspectj.AbstractTransactionAspect; SearchStrategy: all) did not find any beans of type org.springframework.transaction.aspectj.AbstractTransactionAspect (OnBeanCondition) + + TransactionAutoConfiguration.EnableTransactionManagementConfiguration.JdkDynamicAutoProxyConfiguration: + Did not match: + - @ConditionalOnProperty (spring.aop.proxy-target-class=false) did not find property 'proxy-target-class' (OnPropertyCondition) + + UserDetailsServiceAutoConfiguration: + Did not match: + - @ConditionalOnMissingBean (types: org.springframework.security.authentication.AuthenticationManager,org.springframework.security.authentication.AuthenticationProvider,org.springframework.security.core.userdetails.UserDetailsService,org.springframework.security.authentication.AuthenticationManagerResolver,org.springframework.security.oauth2.jwt.JwtDecoder; SearchStrategy: all) found beans of type 'org.springframework.security.authentication.AuthenticationManager' authenticationManager and found beans of type 'org.springframework.security.core.userdetails.UserDetailsService' customUserDetailsService and found beans of type 'org.springframework.security.authentication.AuthenticationProvider' authenticationProvider (OnBeanCondition) + Matched: + - @ConditionalOnClass found required class 'org.springframework.security.authentication.AuthenticationManager' (OnClassCondition) + - AnyNestedCondition 1 matched 2 did not; NestedCondition on UserDetailsServiceAutoConfiguration.MissingAlternativeOrUserPropertiesConfigured.PasswordConfigured @ConditionalOnProperty (spring.security.user.password) did not find property 'password'; NestedCondition on UserDetailsServiceAutoConfiguration.MissingAlternativeOrUserPropertiesConfigured.NameConfigured @ConditionalOnProperty (spring.security.user.name) did not find property 'name'; NestedCondition on UserDetailsServiceAutoConfiguration.MissingAlternativeOrUserPropertiesConfigured.MissingAlternative @ConditionalOnMissingClass did not find unwanted classes 'org.springframework.security.oauth2.client.registration.ClientRegistrationRepository', 'org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector', 'org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository' (UserDetailsServiceAutoConfiguration.MissingAlternativeOrUserPropertiesConfigured) + + WebClientAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.web.reactive.function.client.WebClient' (OnClassCondition) + + WebFluxAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.web.reactive.config.WebFluxConfigurer' (OnClassCondition) + + WebMvcAutoConfiguration#hiddenHttpMethodFilter: + Did not match: + - @ConditionalOnProperty (spring.mvc.hiddenmethod.filter.enabled) did not find property 'enabled' (OnPropertyCondition) + + WebMvcAutoConfiguration.ProblemDetailsErrorHandlingConfiguration: + Did not match: + - @ConditionalOnProperty (spring.mvc.problemdetails.enabled=true) did not find property 'enabled' (OnPropertyCondition) + + WebMvcAutoConfiguration.ResourceChainCustomizerConfiguration: + Did not match: + - @ConditionalOnEnabledResourceChain did not find class org.webjars.WebJarAssetLocator (OnEnabledResourceChainCondition) + + WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#beanNameViewResolver: + Did not match: + - @ConditionalOnMissingBean (types: org.springframework.web.servlet.view.BeanNameViewResolver; SearchStrategy: all) found beans of type 'org.springframework.web.servlet.view.BeanNameViewResolver' beanNameViewResolver (OnBeanCondition) + + WebServiceTemplateAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.oxm.Marshaller' (OnClassCondition) + + WebServicesAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.ws.transport.http.MessageDispatcherServlet' (OnClassCondition) + + WebSessionIdResolverAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'reactor.core.publisher.Mono' (OnClassCondition) + + WebSocketMessagingAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer' (OnClassCondition) + + WebSocketReactiveAutoConfiguration: + Did not match: + - @ConditionalOnWebApplication did not find reactive web application classes (OnWebApplicationCondition) + + WebSocketServletAutoConfiguration.JettyWebSocketConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'org.eclipse.jetty.ee10.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer' (OnClassCondition) + + WebSocketServletAutoConfiguration.UndertowWebSocketConfiguration: + Did not match: + - @ConditionalOnClass did not find required class 'io.undertow.websockets.jsr.Bootstrap' (OnClassCondition) + + XADataSourceAutoConfiguration: + Did not match: + - @ConditionalOnBean (types: org.springframework.boot.jdbc.XADataSourceWrapper; SearchStrategy: all) did not find any beans of type org.springframework.boot.jdbc.XADataSourceWrapper (OnBeanCondition) + Matched: + - @ConditionalOnClass found required classes 'javax.sql.DataSource', 'jakarta.transaction.TransactionManager', 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType' (OnClassCondition) + + +Exclusions: +----------- + + None + + +Unconditional classes: +---------------------- + + org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration + + org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration + + org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration + + org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration + + org.springdoc.core.configuration.SpringDocSpecPropertiesConfiguration + + org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration + + org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration + + + +2026-03-09 19:29:10.492 [main] INFO com.yoyuzh.PortalBackendApplication - Started PortalBackendApplication in 10.854 seconds (process running for 11.553) +2026-03-09 19:29:10.493 [main] DEBUG o.s.b.a.ApplicationAvailabilityBean - Application availability state LivenessState changed to CORRECT +2026-03-09 19:29:10.494 [main] DEBUG o.s.b.a.ApplicationAvailabilityBean - Application availability state ReadinessState changed to ACCEPTING_TRAFFIC +2026-03-09 19:29:26.569 [http-nio-8080-exec-1] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet' +2026-03-09 19:29:26.571 [http-nio-8080-exec-1] INFO o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet' +2026-03-09 19:29:26.572 [http-nio-8080-exec-1] DEBUG o.s.web.servlet.DispatcherServlet - Detected StandardServletMultipartResolver +2026-03-09 19:29:26.572 [http-nio-8080-exec-1] DEBUG o.s.web.servlet.DispatcherServlet - Detected AcceptHeaderLocaleResolver +2026-03-09 19:29:26.572 [http-nio-8080-exec-1] DEBUG o.s.web.servlet.DispatcherServlet - Detected FixedThemeResolver +2026-03-09 19:29:26.572 [http-nio-8080-exec-1] DEBUG o.s.web.servlet.DispatcherServlet - Detected org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator@16e3a461 +2026-03-09 19:29:26.573 [http-nio-8080-exec-1] DEBUG o.s.web.servlet.DispatcherServlet - Detected org.springframework.web.servlet.support.SessionFlashMapManager@6dbf203c +2026-03-09 19:29:26.573 [http-nio-8080-exec-1] DEBUG o.s.web.servlet.DispatcherServlet - enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data +2026-03-09 19:29:26.573 [http-nio-8080-exec-1] INFO o.s.web.servlet.DispatcherServlet - Completed initialization in 1 ms +2026-03-09 19:29:26.587 [http-nio-8080-exec-1] DEBUG o.s.web.servlet.DispatcherServlet - POST "/api/auth/dev-login?username=recentqa", parameters={masked} +2026-03-09 19:29:26.587 [http-nio-8080-exec-1] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to com.yoyuzh.auth.DevAuthController#devLogin(String) +2026-03-09 19:29:26.684 [http-nio-8080-exec-1] DEBUG org.hibernate.SQL - + select + u1_0.id, + u1_0.created_at, + u1_0.email, + u1_0.password_hash, + u1_0.username + from + portal_user u1_0 + where + u1_0.username=? +2026-03-09 19:29:26.795 [http-nio-8080-exec-1] DEBUG org.hibernate.SQL - + insert + into + portal_user + (created_at, email, password_hash, username, id) + values + (?, ?, ?, ?, default) +2026-03-09 19:29:26.959 [http-nio-8080-exec-1] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'application/json', given [*/*] and supported [application/json, application/*+json] +2026-03-09 19:29:26.973 [http-nio-8080-exec-1] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing [ApiResponse[code=0, msg=success, data=AuthResponse[token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyZWNlbnRxYS (truncated)...] +2026-03-09 19:29:26.987 [http-nio-8080-exec-1] DEBUG o.s.web.servlet.DispatcherServlet - Completed 200 OK +2026-03-09 19:29:27.065 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - + select + u1_0.id, + u1_0.created_at, + u1_0.email, + u1_0.password_hash, + u1_0.username + from + portal_user u1_0 + where + u1_0.username=? +2026-03-09 19:29:27.073 [http-nio-8080-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - GET "/api/files/recent", parameters={} +2026-03-09 19:29:27.073 [http-nio-8080-exec-2] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to com.yoyuzh.files.FileController#recent(UserDetails) +2026-03-09 19:29:27.075 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - + select + u1_0.id, + u1_0.created_at, + u1_0.email, + u1_0.password_hash, + u1_0.username + from + portal_user u1_0 + where + u1_0.username=? +2026-03-09 19:29:27.080 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - + select + sf1_0.id, + sf1_0.content_type, + sf1_0.created_at, + sf1_0.is_directory, + sf1_0.filename, + sf1_0.path, + sf1_0.size, + sf1_0.storage_name, + sf1_0.user_id + from + portal_file sf1_0 + where + sf1_0.user_id=? + and not(sf1_0.is_directory) + order by + sf1_0.created_at desc + fetch + first ? rows only +2026-03-09 19:29:27.085 [http-nio-8080-exec-2] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'application/json', given [*/*] and supported [application/json, application/*+json] +2026-03-09 19:29:27.085 [http-nio-8080-exec-2] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing [ApiResponse[code=0, msg=success, data=[]]] +2026-03-09 19:29:27.086 [http-nio-8080-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - Completed 200 OK +2026-03-09 19:29:28.586 [http-nio-8080-exec-4] DEBUG o.s.web.servlet.DispatcherServlet - GET "/swagger-ui.html", parameters={} +2026-03-09 19:29:28.588 [http-nio-8080-exec-4] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to org.springdoc.webmvc.ui.SwaggerWelcomeWebMvc#redirectToUi(HttpServletRequest) +2026-03-09 19:29:28.606 [http-nio-8080-exec-4] DEBUG o.s.w.s.m.m.a.HttpEntityMethodProcessor - Using 'application/json', given [*/*] and supported [application/json, application/*+json] +2026-03-09 19:29:28.607 [http-nio-8080-exec-4] DEBUG o.s.w.s.m.m.a.HttpEntityMethodProcessor - Nothing to write: null body +2026-03-09 19:29:28.608 [http-nio-8080-exec-4] DEBUG o.s.web.servlet.DispatcherServlet - Completed 302 FOUND +2026-03-09 19:29:28.614 [http-nio-8080-exec-7] DEBUG o.s.web.servlet.DispatcherServlet - GET "/swagger-ui/index.html", parameters={} +2026-03-09 19:29:28.615 [http-nio-8080-exec-7] DEBUG o.s.w.s.h.SimpleUrlHandlerMapping - Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/webjars/]] +2026-03-09 19:29:28.620 [http-nio-8080-exec-7] DEBUG o.s.web.servlet.DispatcherServlet - Completed 200 OK +2026-03-09 19:29:46.174 [http-nio-8080-exec-3] DEBUG o.s.web.servlet.DispatcherServlet - POST "/api/auth/dev-login?username=recentqa", parameters={masked} +2026-03-09 19:29:46.174 [http-nio-8080-exec-3] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to com.yoyuzh.auth.DevAuthController#devLogin(String) +2026-03-09 19:29:46.175 [http-nio-8080-exec-3] DEBUG org.hibernate.SQL - + select + u1_0.id, + u1_0.created_at, + u1_0.email, + u1_0.password_hash, + u1_0.username + from + portal_user u1_0 + where + u1_0.username=? +2026-03-09 19:29:46.176 [http-nio-8080-exec-3] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'application/json', given [*/*] and supported [application/json, application/*+json] +2026-03-09 19:29:46.177 [http-nio-8080-exec-3] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing [ApiResponse[code=0, msg=success, data=AuthResponse[token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyZWNlbnRxYS (truncated)...] +2026-03-09 19:29:46.177 [http-nio-8080-exec-3] DEBUG o.s.web.servlet.DispatcherServlet - Completed 200 OK +2026-03-09 19:29:46.288 [http-nio-8080-exec-8] DEBUG org.hibernate.SQL - + select + u1_0.id, + u1_0.created_at, + u1_0.email, + u1_0.password_hash, + u1_0.username + from + portal_user u1_0 + where + u1_0.username=? +2026-03-09 19:29:46.290 [http-nio-8080-exec-8] DEBUG o.s.web.servlet.DispatcherServlet - GET "/api/files/recent", parameters={} +2026-03-09 19:29:46.290 [http-nio-8080-exec-8] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to com.yoyuzh.files.FileController#recent(UserDetails) +2026-03-09 19:29:46.291 [http-nio-8080-exec-8] DEBUG org.hibernate.SQL - + select + u1_0.id, + u1_0.created_at, + u1_0.email, + u1_0.password_hash, + u1_0.username + from + portal_user u1_0 + where + u1_0.username=? +2026-03-09 19:29:46.293 [http-nio-8080-exec-8] DEBUG org.hibernate.SQL - + select + sf1_0.id, + sf1_0.content_type, + sf1_0.created_at, + sf1_0.is_directory, + sf1_0.filename, + sf1_0.path, + sf1_0.size, + sf1_0.storage_name, + sf1_0.user_id + from + portal_file sf1_0 + where + sf1_0.user_id=? + and not(sf1_0.is_directory) + order by + sf1_0.created_at desc + fetch + first ? rows only +2026-03-09 19:29:46.293 [http-nio-8080-exec-8] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'application/json', given [*/*] and supported [application/json, application/*+json] +2026-03-09 19:29:46.294 [http-nio-8080-exec-8] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing [ApiResponse[code=0, msg=success, data=[]]] +2026-03-09 19:29:46.294 [http-nio-8080-exec-8] DEBUG o.s.web.servlet.DispatcherServlet - Completed 200 OK +2026-03-09 19:30:02.083 [http-nio-8080-exec-10] DEBUG o.s.web.servlet.DispatcherServlet - POST "/api/auth/dev-login?username=recentqa", parameters={masked} +2026-03-09 19:30:02.084 [http-nio-8080-exec-10] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to com.yoyuzh.auth.DevAuthController#devLogin(String) +2026-03-09 19:30:02.085 [http-nio-8080-exec-10] DEBUG org.hibernate.SQL - + select + u1_0.id, + u1_0.created_at, + u1_0.email, + u1_0.password_hash, + u1_0.username + from + portal_user u1_0 + where + u1_0.username=? +2026-03-09 19:30:02.085 [http-nio-8080-exec-10] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'application/json', given [*/*] and supported [application/json, application/*+json] +2026-03-09 19:30:02.085 [http-nio-8080-exec-10] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing [ApiResponse[code=0, msg=success, data=AuthResponse[token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyZWNlbnRxYS (truncated)...] +2026-03-09 19:30:02.085 [http-nio-8080-exec-10] DEBUG o.s.web.servlet.DispatcherServlet - Completed 200 OK +2026-03-09 19:30:02.247 [http-nio-8080-exec-1] DEBUG org.hibernate.SQL - + select + u1_0.id, + u1_0.created_at, + u1_0.email, + u1_0.password_hash, + u1_0.username + from + portal_user u1_0 + where + u1_0.username=? +2026-03-09 19:30:02.248 [http-nio-8080-exec-1] DEBUG o.s.web.servlet.DispatcherServlet - POST "/api/files/upload", parameters={multipart} +2026-03-09 19:30:02.268 [http-nio-8080-exec-1] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to com.yoyuzh.files.FileController#upload(UserDetails, String, MultipartFile) +2026-03-09 19:30:02.272 [http-nio-8080-exec-1] DEBUG org.hibernate.SQL - + select + u1_0.id, + u1_0.created_at, + u1_0.email, + u1_0.password_hash, + u1_0.username + from + portal_user u1_0 + where + u1_0.username=? +2026-03-09 19:30:02.287 [http-nio-8080-exec-1] DEBUG org.hibernate.SQL - + select + case + when count(sf1_0.id)>0 + then true + else false + end +from + portal_file sf1_0 +where + sf1_0.user_id=? + and sf1_0.path=? + and sf1_0.filename=? +2026-03-09 19:30:02.322 [http-nio-8080-exec-1] DEBUG org.hibernate.SQL - + insert + into + portal_file + (content_type, created_at, is_directory, filename, path, size, storage_name, user_id, id) + values + (?, ?, ?, ?, ?, ?, ?, ?, default) +2026-03-09 19:30:02.327 [http-nio-8080-exec-1] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'application/json', given [*/*] and supported [application/json, application/*+json] +2026-03-09 19:30:02.332 [http-nio-8080-exec-1] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing [ApiResponse[code=0, msg=success, data=FileMetadataResponse[id=16, filename=recent-smoke.txt, path=/, (truncated)...] +2026-03-09 19:30:02.332 [http-nio-8080-exec-1] DEBUG o.s.web.servlet.DispatcherServlet - Completed 200 OK +2026-03-09 19:30:02.346 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - + select + u1_0.id, + u1_0.created_at, + u1_0.email, + u1_0.password_hash, + u1_0.username + from + portal_user u1_0 + where + u1_0.username=? +2026-03-09 19:30:02.346 [http-nio-8080-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - GET "/api/files/recent", parameters={} +2026-03-09 19:30:02.346 [http-nio-8080-exec-2] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to com.yoyuzh.files.FileController#recent(UserDetails) +2026-03-09 19:30:02.352 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - + select + u1_0.id, + u1_0.created_at, + u1_0.email, + u1_0.password_hash, + u1_0.username + from + portal_user u1_0 + where + u1_0.username=? +2026-03-09 19:30:02.352 [http-nio-8080-exec-2] DEBUG org.hibernate.SQL - + select + sf1_0.id, + sf1_0.content_type, + sf1_0.created_at, + sf1_0.is_directory, + sf1_0.filename, + sf1_0.path, + sf1_0.size, + sf1_0.storage_name, + sf1_0.user_id + from + portal_file sf1_0 + where + sf1_0.user_id=? + and not(sf1_0.is_directory) + order by + sf1_0.created_at desc + fetch + first ? rows only +2026-03-09 19:30:02.352 [http-nio-8080-exec-2] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'application/json', given [*/*] and supported [application/json, application/*+json] +2026-03-09 19:30:02.352 [http-nio-8080-exec-2] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing [ApiResponse[code=0, msg=success, data=[FileMetadataResponse[id=16, filename=recent-smoke.txt, path=/ (truncated)...] +2026-03-09 19:30:02.362 [http-nio-8080-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - Completed 200 OK diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..636993a --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,108 @@ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.8 + + + + com.yoyuzh + yoyuzh-portal-backend + 0.0.1-SNAPSHOT + yoyuzh-portal-backend + Spring Boot backend for yoyuzh.xyz + + + 17 + 0.12.6 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + com.mysql + mysql-connector-j + runtime + + + org.postgresql + postgresql + runtime + + + com.h2database + h2 + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + org.mockito + mockito-junit-jupiter + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/backend/sql/mysql-init.sql b/backend/sql/mysql-init.sql new file mode 100644 index 0000000..ec97608 --- /dev/null +++ b/backend/sql/mysql-init.sql @@ -0,0 +1,54 @@ +CREATE DATABASE IF NOT EXISTS yoyuzh_portal DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE yoyuzh_portal; + +CREATE TABLE IF NOT EXISTS portal_user ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + username VARCHAR(64) NOT NULL, + email VARCHAR(128) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT uk_portal_user_username UNIQUE (username), + CONSTRAINT uk_portal_user_email UNIQUE (email) +); + +CREATE TABLE IF NOT EXISTS portal_file ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT NOT NULL, + filename VARCHAR(255) NOT NULL, + path VARCHAR(512) NOT NULL, + storage_name VARCHAR(255) NOT NULL, + content_type VARCHAR(255), + size BIGINT NOT NULL, + is_directory BIT NOT NULL DEFAULT b'0', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_portal_file_user FOREIGN KEY (user_id) REFERENCES portal_user (id), + CONSTRAINT uk_portal_file_user_path_name UNIQUE (user_id, path, filename) +); + +CREATE TABLE IF NOT EXISTS portal_course ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT NOT NULL, + course_name VARCHAR(255) NOT NULL, + teacher VARCHAR(255), + classroom VARCHAR(255), + day_of_week INT, + start_time INT, + end_time INT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_portal_course_user FOREIGN KEY (user_id) REFERENCES portal_user (id) +); + +CREATE TABLE IF NOT EXISTS portal_grade ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT NOT NULL, + course_name VARCHAR(255) NOT NULL, + grade DOUBLE NOT NULL, + semester VARCHAR(64) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_portal_grade_user FOREIGN KEY (user_id) REFERENCES portal_user (id) +); + +CREATE INDEX idx_user_created_at ON portal_user (created_at); +CREATE INDEX idx_file_created_at ON portal_file (created_at); +CREATE INDEX idx_course_user_created ON portal_course (user_id, created_at); +CREATE INDEX idx_grade_user_created ON portal_grade (user_id, created_at); diff --git a/backend/sql/opengauss-init.sql b/backend/sql/opengauss-init.sql new file mode 100644 index 0000000..01eeefa --- /dev/null +++ b/backend/sql/opengauss-init.sql @@ -0,0 +1,46 @@ +CREATE TABLE IF NOT EXISTS portal_user ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR(64) NOT NULL UNIQUE, + email VARCHAR(128) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS portal_file ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES portal_user (id), + filename VARCHAR(255) NOT NULL, + path VARCHAR(512) NOT NULL, + storage_name VARCHAR(255) NOT NULL, + content_type VARCHAR(255), + size BIGINT NOT NULL, + is_directory BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT uk_portal_file_user_path_name UNIQUE (user_id, path, filename) +); + +CREATE TABLE IF NOT EXISTS portal_course ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES portal_user (id), + course_name VARCHAR(255) NOT NULL, + teacher VARCHAR(255), + classroom VARCHAR(255), + day_of_week INTEGER, + start_time INTEGER, + end_time INTEGER, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS portal_grade ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES portal_user (id), + course_name VARCHAR(255) NOT NULL, + grade DOUBLE PRECISION NOT NULL, + semester VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_user_created_at ON portal_user (created_at); +CREATE INDEX IF NOT EXISTS idx_file_created_at ON portal_file (created_at); +CREATE INDEX IF NOT EXISTS idx_course_user_created ON portal_course (user_id, created_at); +CREATE INDEX IF NOT EXISTS idx_grade_user_created ON portal_grade (user_id, created_at); diff --git a/backend/src/main/java/com/yoyuzh/PortalBackendApplication.java b/backend/src/main/java/com/yoyuzh/PortalBackendApplication.java new file mode 100644 index 0000000..3a67fdb --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/PortalBackendApplication.java @@ -0,0 +1,21 @@ +package com.yoyuzh; + +import com.yoyuzh.config.CquApiProperties; +import com.yoyuzh.config.FileStorageProperties; +import com.yoyuzh.config.JwtProperties; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +@SpringBootApplication +@EnableConfigurationProperties({ + JwtProperties.class, + FileStorageProperties.class, + CquApiProperties.class +}) +public class PortalBackendApplication { + + public static void main(String[] args) { + SpringApplication.run(PortalBackendApplication.class, args); + } +} diff --git a/backend/src/main/java/com/yoyuzh/auth/AuthController.java b/backend/src/main/java/com/yoyuzh/auth/AuthController.java new file mode 100644 index 0000000..f5242d8 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/auth/AuthController.java @@ -0,0 +1,33 @@ +package com.yoyuzh.auth; + +import com.yoyuzh.auth.dto.AuthResponse; +import com.yoyuzh.auth.dto.LoginRequest; +import com.yoyuzh.auth.dto.RegisterRequest; +import com.yoyuzh.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @Operation(summary = "用户注册") + @PostMapping("/register") + public ApiResponse register(@Valid @RequestBody RegisterRequest request) { + return ApiResponse.success(authService.register(request)); + } + + @Operation(summary = "用户登录") + @PostMapping("/login") + public ApiResponse login(@Valid @RequestBody LoginRequest request) { + return ApiResponse.success(authService.login(request)); + } +} diff --git a/backend/src/main/java/com/yoyuzh/auth/AuthService.java b/backend/src/main/java/com/yoyuzh/auth/AuthService.java new file mode 100644 index 0000000..8aef00e --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/auth/AuthService.java @@ -0,0 +1,83 @@ +package com.yoyuzh.auth; + +import com.yoyuzh.auth.dto.AuthResponse; +import com.yoyuzh.auth.dto.LoginRequest; +import com.yoyuzh.auth.dto.RegisterRequest; +import com.yoyuzh.auth.dto.UserProfileResponse; +import com.yoyuzh.common.BusinessException; +import com.yoyuzh.common.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final AuthenticationManager authenticationManager; + private final JwtTokenProvider jwtTokenProvider; + + @Transactional + public AuthResponse register(RegisterRequest request) { + if (userRepository.existsByUsername(request.username())) { + throw new BusinessException(ErrorCode.UNKNOWN, "用户名已存在"); + } + if (userRepository.existsByEmail(request.email())) { + throw new BusinessException(ErrorCode.UNKNOWN, "邮箱已存在"); + } + + User user = new User(); + user.setUsername(request.username()); + user.setEmail(request.email()); + user.setPasswordHash(passwordEncoder.encode(request.password())); + User saved = userRepository.save(user); + return new AuthResponse(jwtTokenProvider.generateToken(saved.getId(), saved.getUsername()), toProfile(saved)); + } + + public AuthResponse login(LoginRequest request) { + try { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.username(), request.password())); + } catch (BadCredentialsException ex) { + throw new BusinessException(ErrorCode.NOT_LOGGED_IN, "用户名或密码错误"); + } + + User user = userRepository.findByUsername(request.username()) + .orElseThrow(() -> new BusinessException(ErrorCode.NOT_LOGGED_IN, "用户不存在")); + return new AuthResponse(jwtTokenProvider.generateToken(user.getId(), user.getUsername()), toProfile(user)); + } + + @Transactional + public AuthResponse devLogin(String username) { + String candidate = username == null ? "" : username.trim(); + if (candidate.isEmpty()) { + candidate = "1"; + } + + final String finalCandidate = candidate; + User user = userRepository.findByUsername(finalCandidate).orElseGet(() -> { + User created = new User(); + created.setUsername(finalCandidate); + created.setEmail(finalCandidate + "@dev.local"); + created.setPasswordHash(passwordEncoder.encode("1")); + return userRepository.save(created); + }); + return new AuthResponse(jwtTokenProvider.generateToken(user.getId(), user.getUsername()), toProfile(user)); + } + + public UserProfileResponse getProfile(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new BusinessException(ErrorCode.NOT_LOGGED_IN, "用户不存在")); + return toProfile(user); + } + + private UserProfileResponse toProfile(User user) { + return new UserProfileResponse(user.getId(), user.getUsername(), user.getEmail(), user.getCreatedAt()); + } +} diff --git a/backend/src/main/java/com/yoyuzh/auth/CustomUserDetailsService.java b/backend/src/main/java/com/yoyuzh/auth/CustomUserDetailsService.java new file mode 100644 index 0000000..d74894f --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/auth/CustomUserDetailsService.java @@ -0,0 +1,31 @@ +package com.yoyuzh.auth; + +import com.yoyuzh.common.BusinessException; +import com.yoyuzh.common.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("用户不存在")); + return org.springframework.security.core.userdetails.User.withUsername(user.getUsername()) + .password(user.getPasswordHash()) + .authorities("ROLE_USER") + .build(); + } + + public User loadDomainUser(String username) { + return userRepository.findByUsername(username) + .orElseThrow(() -> new BusinessException(ErrorCode.NOT_LOGGED_IN, "用户不存在")); + } +} diff --git a/backend/src/main/java/com/yoyuzh/auth/DevAuthController.java b/backend/src/main/java/com/yoyuzh/auth/DevAuthController.java new file mode 100644 index 0000000..23f6886 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/auth/DevAuthController.java @@ -0,0 +1,26 @@ +package com.yoyuzh.auth; + +import com.yoyuzh.auth.dto.AuthResponse; +import com.yoyuzh.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Profile("dev") +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class DevAuthController { + + private final AuthService authService; + + @Operation(summary = "开发环境免密登录") + @PostMapping("/dev-login") + public ApiResponse devLogin(@RequestParam(required = false) String username) { + return ApiResponse.success(authService.devLogin(username)); + } +} diff --git a/backend/src/main/java/com/yoyuzh/auth/JwtTokenProvider.java b/backend/src/main/java/com/yoyuzh/auth/JwtTokenProvider.java new file mode 100644 index 0000000..5957045 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/auth/JwtTokenProvider.java @@ -0,0 +1,62 @@ +package com.yoyuzh.auth; + +import com.yoyuzh.config.JwtProperties; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Date; + +@Component +public class JwtTokenProvider { + + private final JwtProperties jwtProperties; + private SecretKey secretKey; + + public JwtTokenProvider(JwtProperties jwtProperties) { + this.jwtProperties = jwtProperties; + } + + @PostConstruct + public void init() { + secretKey = Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(Long userId, String username) { + Instant now = Instant.now(); + return Jwts.builder() + .subject(username) + .claim("uid", userId) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plusSeconds(jwtProperties.getExpirationSeconds()))) + .signWith(secretKey) + .compact(); + } + + public boolean validateToken(String token) { + try { + Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token); + return true; + } catch (Exception ex) { + return false; + } + } + + public String getUsername(String token) { + return parseClaims(token).getSubject(); + } + + public Long getUserId(String token) { + Object uid = parseClaims(token).get("uid"); + return uid == null ? null : Long.parseLong(uid.toString()); + } + + private Claims parseClaims(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload(); + } +} diff --git a/backend/src/main/java/com/yoyuzh/auth/User.java b/backend/src/main/java/com/yoyuzh/auth/User.java new file mode 100644 index 0000000..2cc905c --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/auth/User.java @@ -0,0 +1,84 @@ +package com.yoyuzh.auth; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "portal_user", indexes = { + @Index(name = "uk_user_username", columnList = "username", unique = true), + @Index(name = "uk_user_email", columnList = "email", unique = true), + @Index(name = "idx_user_created_at", columnList = "created_at") +}) +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 64, unique = true) + private String username; + + @Column(nullable = false, length = 128, unique = true) + private String email; + + @Column(name = "password_hash", nullable = false, length = 255) + private String passwordHash; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @PrePersist + public void prePersist() { + if (createdAt == null) { + createdAt = LocalDateTime.now(); + } + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPasswordHash() { + return passwordHash; + } + + public void setPasswordHash(String passwordHash) { + this.passwordHash = passwordHash; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } +} diff --git a/backend/src/main/java/com/yoyuzh/auth/UserController.java b/backend/src/main/java/com/yoyuzh/auth/UserController.java new file mode 100644 index 0000000..7284359 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/auth/UserController.java @@ -0,0 +1,24 @@ +package com.yoyuzh.auth; + +import com.yoyuzh.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/user") +@RequiredArgsConstructor +public class UserController { + + private final AuthService authService; + + @Operation(summary = "获取用户信息") + @GetMapping("/profile") + public ApiResponse profile(@AuthenticationPrincipal UserDetails userDetails) { + return ApiResponse.success(authService.getProfile(userDetails.getUsername())); + } +} diff --git a/backend/src/main/java/com/yoyuzh/auth/UserRepository.java b/backend/src/main/java/com/yoyuzh/auth/UserRepository.java new file mode 100644 index 0000000..0fe4561 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/auth/UserRepository.java @@ -0,0 +1,13 @@ +package com.yoyuzh.auth; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + boolean existsByUsername(String username); + + boolean existsByEmail(String email); + + Optional findByUsername(String username); +} diff --git a/backend/src/main/java/com/yoyuzh/auth/dto/AuthResponse.java b/backend/src/main/java/com/yoyuzh/auth/dto/AuthResponse.java new file mode 100644 index 0000000..b318269 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/auth/dto/AuthResponse.java @@ -0,0 +1,4 @@ +package com.yoyuzh.auth.dto; + +public record AuthResponse(String token, UserProfileResponse user) { +} diff --git a/backend/src/main/java/com/yoyuzh/auth/dto/LoginRequest.java b/backend/src/main/java/com/yoyuzh/auth/dto/LoginRequest.java new file mode 100644 index 0000000..d74da74 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/auth/dto/LoginRequest.java @@ -0,0 +1,9 @@ +package com.yoyuzh.auth.dto; + +import jakarta.validation.constraints.NotBlank; + +public record LoginRequest( + @NotBlank String username, + @NotBlank String password +) { +} diff --git a/backend/src/main/java/com/yoyuzh/auth/dto/RegisterRequest.java b/backend/src/main/java/com/yoyuzh/auth/dto/RegisterRequest.java new file mode 100644 index 0000000..743e333 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/auth/dto/RegisterRequest.java @@ -0,0 +1,12 @@ +package com.yoyuzh.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record RegisterRequest( + @NotBlank @Size(min = 3, max = 64) String username, + @NotBlank @Email @Size(max = 128) String email, + @NotBlank @Size(min = 6, max = 64) String password +) { +} diff --git a/backend/src/main/java/com/yoyuzh/auth/dto/UserProfileResponse.java b/backend/src/main/java/com/yoyuzh/auth/dto/UserProfileResponse.java new file mode 100644 index 0000000..55e5159 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/auth/dto/UserProfileResponse.java @@ -0,0 +1,6 @@ +package com.yoyuzh.auth.dto; + +import java.time.LocalDateTime; + +public record UserProfileResponse(Long id, String username, String email, LocalDateTime createdAt) { +} diff --git a/backend/src/main/java/com/yoyuzh/common/ApiResponse.java b/backend/src/main/java/com/yoyuzh/common/ApiResponse.java new file mode 100644 index 0000000..ca75bc8 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/common/ApiResponse.java @@ -0,0 +1,16 @@ +package com.yoyuzh.common; + +public record ApiResponse(int code, String msg, T data) { + + public static ApiResponse success(T data) { + return new ApiResponse<>(0, "success", data); + } + + public static ApiResponse success() { + return new ApiResponse<>(0, "success", null); + } + + public static ApiResponse error(ErrorCode errorCode, String msg) { + return new ApiResponse<>(errorCode.getCode(), msg, null); + } +} diff --git a/backend/src/main/java/com/yoyuzh/common/BusinessException.java b/backend/src/main/java/com/yoyuzh/common/BusinessException.java new file mode 100644 index 0000000..8189fe1 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/common/BusinessException.java @@ -0,0 +1,15 @@ +package com.yoyuzh.common; + +public class BusinessException extends RuntimeException { + + private final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/backend/src/main/java/com/yoyuzh/common/ErrorCode.java b/backend/src/main/java/com/yoyuzh/common/ErrorCode.java new file mode 100644 index 0000000..1156c53 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/common/ErrorCode.java @@ -0,0 +1,18 @@ +package com.yoyuzh.common; + +public enum ErrorCode { + UNKNOWN(1000), + NOT_LOGGED_IN(1001), + PERMISSION_DENIED(1002), + FILE_NOT_FOUND(1003); + + private final int code; + + ErrorCode(int code) { + this.code = code; + } + + public int getCode() { + return code; + } +} diff --git a/backend/src/main/java/com/yoyuzh/common/GlobalExceptionHandler.java b/backend/src/main/java/com/yoyuzh/common/GlobalExceptionHandler.java new file mode 100644 index 0000000..380ef17 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/common/GlobalExceptionHandler.java @@ -0,0 +1,51 @@ +package com.yoyuzh.common; + +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BusinessException.class) + public ResponseEntity> handleBusinessException(BusinessException ex) { + HttpStatus status = switch (ex.getErrorCode()) { + case NOT_LOGGED_IN -> HttpStatus.UNAUTHORIZED; + case PERMISSION_DENIED -> HttpStatus.FORBIDDEN; + case FILE_NOT_FOUND -> HttpStatus.NOT_FOUND; + default -> HttpStatus.BAD_REQUEST; + }; + return ResponseEntity.status(status).body(ApiResponse.error(ex.getErrorCode(), ex.getMessage())); + } + + @ExceptionHandler({MethodArgumentNotValidException.class, ConstraintViolationException.class}) + public ResponseEntity> handleValidationException(Exception ex) { + return ResponseEntity.badRequest().body(ApiResponse.error(ErrorCode.UNKNOWN, ex.getMessage())); + } + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity> handleAccessDenied(AccessDeniedException ex) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error(ErrorCode.PERMISSION_DENIED, "没有权限访问该资源")); + } + + @ExceptionHandler(BadCredentialsException.class) + public ResponseEntity> handleBadCredentials(BadCredentialsException ex) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(ApiResponse.error(ErrorCode.NOT_LOGGED_IN, "用户名或密码错误")); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleUnknown(Exception ex) { + log.error("Unhandled exception", ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error(ErrorCode.UNKNOWN, "服务器内部错误")); + } +} diff --git a/backend/src/main/java/com/yoyuzh/common/PageResponse.java b/backend/src/main/java/com/yoyuzh/common/PageResponse.java new file mode 100644 index 0000000..3aa7e48 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/common/PageResponse.java @@ -0,0 +1,6 @@ +package com.yoyuzh.common; + +import java.util.List; + +public record PageResponse(List items, long total, int page, int size) { +} diff --git a/backend/src/main/java/com/yoyuzh/config/CquApiProperties.java b/backend/src/main/java/com/yoyuzh/config/CquApiProperties.java new file mode 100644 index 0000000..b4248e0 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/config/CquApiProperties.java @@ -0,0 +1,35 @@ +package com.yoyuzh.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "app.cqu") +public class CquApiProperties { + + private String baseUrl = "https://example-cqu-api.local"; + private boolean requireLogin = false; + private boolean mockEnabled = false; + + public String getBaseUrl() { + return baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public boolean isRequireLogin() { + return requireLogin; + } + + public void setRequireLogin(boolean requireLogin) { + this.requireLogin = requireLogin; + } + + public boolean isMockEnabled() { + return mockEnabled; + } + + public void setMockEnabled(boolean mockEnabled) { + this.mockEnabled = mockEnabled; + } +} diff --git a/backend/src/main/java/com/yoyuzh/config/FileStorageProperties.java b/backend/src/main/java/com/yoyuzh/config/FileStorageProperties.java new file mode 100644 index 0000000..1350d78 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/config/FileStorageProperties.java @@ -0,0 +1,26 @@ +package com.yoyuzh.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "app.storage") +public class FileStorageProperties { + + private String rootDir = "./storage"; + private long maxFileSize = 50 * 1024 * 1024L; + + public String getRootDir() { + return rootDir; + } + + public void setRootDir(String rootDir) { + this.rootDir = rootDir; + } + + public long getMaxFileSize() { + return maxFileSize; + } + + public void setMaxFileSize(long maxFileSize) { + this.maxFileSize = maxFileSize; + } +} diff --git a/backend/src/main/java/com/yoyuzh/config/JwtAuthenticationFilter.java b/backend/src/main/java/com/yoyuzh/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..254e0a5 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/config/JwtAuthenticationFilter.java @@ -0,0 +1,45 @@ +package com.yoyuzh.config; + +import com.yoyuzh.auth.CustomUserDetailsService; +import com.yoyuzh.auth.JwtTokenProvider; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + private final CustomUserDetailsService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + String header = request.getHeader("Authorization"); + if (header != null && header.startsWith("Bearer ")) { + String token = header.substring(7); + if (jwtTokenProvider.validateToken(token) + && SecurityContextHolder.getContext().getAuthentication() == null) { + String username = jwtTokenProvider.getUsername(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + filterChain.doFilter(request, response); + } +} diff --git a/backend/src/main/java/com/yoyuzh/config/JwtProperties.java b/backend/src/main/java/com/yoyuzh/config/JwtProperties.java new file mode 100644 index 0000000..1991f89 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/config/JwtProperties.java @@ -0,0 +1,26 @@ +package com.yoyuzh.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "app.jwt") +public class JwtProperties { + + private String secret = "change-me-change-me-change-me-change-me"; + private long expirationSeconds = 86400; + + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + public long getExpirationSeconds() { + return expirationSeconds; + } + + public void setExpirationSeconds(long expirationSeconds) { + this.expirationSeconds = expirationSeconds; + } +} diff --git a/backend/src/main/java/com/yoyuzh/config/OpenApiConfig.java b/backend/src/main/java/com/yoyuzh/config/OpenApiConfig.java new file mode 100644 index 0000000..72ef7b1 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/config/OpenApiConfig.java @@ -0,0 +1,26 @@ +package com.yoyuzh.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(new Info().title("yoyuzh.xyz Backend API").version("1.0.0").description("Personal portal backend")) + .addSecurityItem(new SecurityRequirement().addList("bearerAuth")) + .components(new Components().addSecuritySchemes("bearerAuth", + new SecurityScheme() + .name("Authorization") + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); + } +} diff --git a/backend/src/main/java/com/yoyuzh/config/RestClientConfig.java b/backend/src/main/java/com/yoyuzh/config/RestClientConfig.java new file mode 100644 index 0000000..0d8cdb2 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/config/RestClientConfig.java @@ -0,0 +1,14 @@ +package com.yoyuzh.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; + +@Configuration +public class RestClientConfig { + + @Bean + public RestClient restClient(RestClient.Builder builder) { + return builder.build(); + } +} diff --git a/backend/src/main/java/com/yoyuzh/config/SecurityConfig.java b/backend/src/main/java/com/yoyuzh/config/SecurityConfig.java new file mode 100644 index 0000000..e78f60e --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/config/SecurityConfig.java @@ -0,0 +1,83 @@ +package com.yoyuzh.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yoyuzh.auth.CustomUserDetailsService; +import com.yoyuzh.common.ApiResponse; +import com.yoyuzh.common.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CustomUserDetailsService userDetailsService; + private final ObjectMapper objectMapper; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .cors(Customizer.withDefaults()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/auth/**", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html") + .permitAll() + .requestMatchers("/api/files/**", "/api/user/**") + .authenticated() + .anyRequest() + .permitAll()) + .authenticationProvider(authenticationProvider()) + .exceptionHandling(ex -> ex + .authenticationEntryPoint((request, response, e) -> { + response.setStatus(401); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + objectMapper.writeValue(response.getWriter(), + ApiResponse.error(ErrorCode.NOT_LOGGED_IN, "用户未登录")); + }) + .accessDeniedHandler((request, response, e) -> { + response.setStatus(403); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + objectMapper.writeValue(response.getWriter(), + ApiResponse.error(ErrorCode.PERMISSION_DENIED, "权限不足")); + })) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailsService); + provider.setPasswordEncoder(passwordEncoder()); + return provider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/backend/src/main/java/com/yoyuzh/cqu/Course.java b/backend/src/main/java/com/yoyuzh/cqu/Course.java new file mode 100644 index 0000000..9432cf3 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/cqu/Course.java @@ -0,0 +1,154 @@ +package com.yoyuzh.cqu; + +import com.yoyuzh.auth.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "portal_course", indexes = { + @Index(name = "idx_course_user_semester", columnList = "user_id,semester,student_id"), + @Index(name = "idx_course_user_created", columnList = "user_id,created_at") +}) +public class Course { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "course_name", nullable = false, length = 255) + private String courseName; + + @Column(length = 64) + private String semester; + + @Column(name = "student_id", length = 64) + private String studentId; + + @Column(length = 255) + private String teacher; + + @Column(length = 255) + private String classroom; + + @Column(name = "day_of_week") + private Integer dayOfWeek; + + @Column(name = "start_time") + private Integer startTime; + + @Column(name = "end_time") + private Integer endTime; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @PrePersist + public void prePersist() { + if (createdAt == null) { + createdAt = LocalDateTime.now(); + } + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public String getCourseName() { + return courseName; + } + + public void setCourseName(String courseName) { + this.courseName = courseName; + } + + public String getSemester() { + return semester; + } + + public void setSemester(String semester) { + this.semester = semester; + } + + public String getStudentId() { + return studentId; + } + + public void setStudentId(String studentId) { + this.studentId = studentId; + } + + public String getTeacher() { + return teacher; + } + + public void setTeacher(String teacher) { + this.teacher = teacher; + } + + public String getClassroom() { + return classroom; + } + + public void setClassroom(String classroom) { + this.classroom = classroom; + } + + public Integer getDayOfWeek() { + return dayOfWeek; + } + + public void setDayOfWeek(Integer dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } + + public Integer getStartTime() { + return startTime; + } + + public void setStartTime(Integer startTime) { + this.startTime = startTime; + } + + public Integer getEndTime() { + return endTime; + } + + public void setEndTime(Integer endTime) { + this.endTime = endTime; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } +} diff --git a/backend/src/main/java/com/yoyuzh/cqu/CourseRepository.java b/backend/src/main/java/com/yoyuzh/cqu/CourseRepository.java new file mode 100644 index 0000000..5510b5d --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/cqu/CourseRepository.java @@ -0,0 +1,11 @@ +package com.yoyuzh.cqu; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CourseRepository extends JpaRepository { + List findByUserIdAndStudentIdAndSemesterOrderByDayOfWeekAscStartTimeAsc(Long userId, String studentId, String semester); + + void deleteByUserIdAndStudentIdAndSemester(Long userId, String studentId, String semester); +} diff --git a/backend/src/main/java/com/yoyuzh/cqu/CourseResponse.java b/backend/src/main/java/com/yoyuzh/cqu/CourseResponse.java new file mode 100644 index 0000000..ea95fa2 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/cqu/CourseResponse.java @@ -0,0 +1,11 @@ +package com.yoyuzh.cqu; + +public record CourseResponse( + String courseName, + String teacher, + String classroom, + Integer dayOfWeek, + Integer startTime, + Integer endTime +) { +} diff --git a/backend/src/main/java/com/yoyuzh/cqu/CquApiClient.java b/backend/src/main/java/com/yoyuzh/cqu/CquApiClient.java new file mode 100644 index 0000000..b257bfb --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/cqu/CquApiClient.java @@ -0,0 +1,40 @@ +package com.yoyuzh.cqu; + +import com.yoyuzh.config.CquApiProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import java.util.List; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class CquApiClient { + + private final RestClient restClient; + private final CquApiProperties properties; + + public List> fetchSchedule(String semester, String studentId) { + if (properties.isMockEnabled()) { + return CquMockDataFactory.createSchedule(semester, studentId); + } + return restClient.get() + .uri(properties.getBaseUrl() + "/schedule?semester={semester}&studentId={studentId}", semester, studentId) + .retrieve() + .body(new ParameterizedTypeReference<>() { + }); + } + + public List> fetchGrades(String semester, String studentId) { + if (properties.isMockEnabled()) { + return CquMockDataFactory.createGrades(semester, studentId); + } + return restClient.get() + .uri(properties.getBaseUrl() + "/grades?semester={semester}&studentId={studentId}", semester, studentId) + .retrieve() + .body(new ParameterizedTypeReference<>() { + }); + } +} diff --git a/backend/src/main/java/com/yoyuzh/cqu/CquController.java b/backend/src/main/java/com/yoyuzh/cqu/CquController.java new file mode 100644 index 0000000..dc7645f --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/cqu/CquController.java @@ -0,0 +1,44 @@ +package com.yoyuzh.cqu; + +import com.yoyuzh.auth.CustomUserDetailsService; +import com.yoyuzh.auth.User; +import com.yoyuzh.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/cqu") +@RequiredArgsConstructor +public class CquController { + + private final CquDataService cquDataService; + private final CustomUserDetailsService userDetailsService; + + @Operation(summary = "获取课表") + @GetMapping("/schedule") + public ApiResponse> schedule(@AuthenticationPrincipal UserDetails userDetails, + @RequestParam String semester, + @RequestParam String studentId) { + return ApiResponse.success(cquDataService.getSchedule(resolveUser(userDetails), semester, studentId)); + } + + @Operation(summary = "获取成绩") + @GetMapping("/grades") + public ApiResponse> grades(@AuthenticationPrincipal UserDetails userDetails, + @RequestParam String semester, + @RequestParam String studentId) { + return ApiResponse.success(cquDataService.getGrades(resolveUser(userDetails), semester, studentId)); + } + + private User resolveUser(UserDetails userDetails) { + return userDetails == null ? null : userDetailsService.loadDomainUser(userDetails.getUsername()); + } +} diff --git a/backend/src/main/java/com/yoyuzh/cqu/CquDataService.java b/backend/src/main/java/com/yoyuzh/cqu/CquDataService.java new file mode 100644 index 0000000..e854627 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/cqu/CquDataService.java @@ -0,0 +1,129 @@ +package com.yoyuzh.cqu; + +import com.yoyuzh.auth.User; +import com.yoyuzh.common.BusinessException; +import com.yoyuzh.common.ErrorCode; +import com.yoyuzh.config.CquApiProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class CquDataService { + + private final CquApiClient cquApiClient; + private final CourseRepository courseRepository; + private final GradeRepository gradeRepository; + private final CquApiProperties cquApiProperties; + + public List getSchedule(User user, String semester, String studentId) { + requireLoginIfNecessary(user); + List responses = cquApiClient.fetchSchedule(semester, studentId).stream() + .map(this::toCourseResponse) + .toList(); + if (user != null) { + saveCourses(user, semester, studentId, responses); + return courseRepository.findByUserIdAndStudentIdAndSemesterOrderByDayOfWeekAscStartTimeAsc( + user.getId(), studentId, semester) + .stream() + .map(item -> new CourseResponse( + item.getCourseName(), + item.getTeacher(), + item.getClassroom(), + item.getDayOfWeek(), + item.getStartTime(), + item.getEndTime())) + .toList(); + } + return responses; + } + + public List getGrades(User user, String semester, String studentId) { + requireLoginIfNecessary(user); + List responses = cquApiClient.fetchGrades(semester, studentId).stream() + .map(this::toGradeResponse) + .toList(); + if (user != null) { + saveGrades(user, semester, studentId, responses); + return gradeRepository.findByUserIdAndStudentIdOrderBySemesterAscGradeDesc(user.getId(), studentId) + .stream() + .map(item -> new GradeResponse(item.getCourseName(), item.getGrade(), item.getSemester())) + .toList(); + } + return responses; + } + + private void requireLoginIfNecessary(User user) { + if (cquApiProperties.isRequireLogin() && user == null) { + throw new BusinessException(ErrorCode.NOT_LOGGED_IN, "该接口需要登录后访问"); + } + } + + @Transactional + protected void saveCourses(User user, String semester, String studentId, List responses) { + courseRepository.deleteByUserIdAndStudentIdAndSemester(user.getId(), studentId, semester); + courseRepository.saveAll(responses.stream().map(item -> { + Course course = new Course(); + course.setUser(user); + course.setCourseName(item.courseName()); + course.setSemester(semester); + course.setStudentId(studentId); + course.setTeacher(item.teacher()); + course.setClassroom(item.classroom()); + course.setDayOfWeek(item.dayOfWeek()); + course.setStartTime(item.startTime()); + course.setEndTime(item.endTime()); + return course; + }).toList()); + } + + @Transactional + protected void saveGrades(User user, String semester, String studentId, List responses) { + gradeRepository.deleteByUserIdAndStudentIdAndSemester(user.getId(), studentId, semester); + gradeRepository.saveAll(responses.stream().map(item -> { + Grade grade = new Grade(); + grade.setUser(user); + grade.setCourseName(item.courseName()); + grade.setGrade(item.grade()); + grade.setSemester(item.semester() == null ? semester : item.semester()); + grade.setStudentId(studentId); + return grade; + }).toList()); + } + + private CourseResponse toCourseResponse(Map source) { + return new CourseResponse( + stringValue(source, "courseName"), + stringValue(source, "teacher"), + stringValue(source, "classroom"), + intValue(source, "dayOfWeek"), + intValue(source, "startTime"), + intValue(source, "endTime")); + } + + private GradeResponse toGradeResponse(Map source) { + return new GradeResponse( + stringValue(source, "courseName"), + doubleValue(source, "grade"), + stringValue(source, "semester")); + } + + private String stringValue(Map source, String key) { + Object value = source.get(key); + return value == null ? null : value.toString(); + } + + private Integer intValue(Map source, String key) { + Object value = source.get(key); + return value == null ? null : Integer.parseInt(value.toString()); + } + + private Double doubleValue(Map source, String key) { + Object value = source.get(key); + return value == null ? null : Double.parseDouble(value.toString()); + } +} diff --git a/backend/src/main/java/com/yoyuzh/cqu/CquMockDataFactory.java b/backend/src/main/java/com/yoyuzh/cqu/CquMockDataFactory.java new file mode 100644 index 0000000..df78061 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/cqu/CquMockDataFactory.java @@ -0,0 +1,68 @@ +package com.yoyuzh.cqu; + +import java.util.List; +import java.util.Map; + +public final class CquMockDataFactory { + + private CquMockDataFactory() { + } + + public static List> createSchedule(String semester, String studentId) { + return List.of( + Map.of( + "studentId", studentId, + "semester", semester, + "courseName", "高级 Java 程序设计", + "teacher", "李老师", + "classroom", "D1131", + "dayOfWeek", 1, + "startTime", 1, + "endTime", 2 + ), + Map.of( + "studentId", studentId, + "semester", semester, + "courseName", "计算机网络", + "teacher", "王老师", + "classroom", "A2204", + "dayOfWeek", 3, + "startTime", 3, + "endTime", 4 + ), + Map.of( + "studentId", studentId, + "semester", semester, + "courseName", "软件工程", + "teacher", "周老师", + "classroom", "B3102", + "dayOfWeek", 5, + "startTime", 5, + "endTime", 6 + ) + ); + } + + public static List> createGrades(String semester, String studentId) { + return List.of( + Map.of( + "studentId", studentId, + "semester", semester, + "courseName", "高级 Java 程序设计", + "grade", 92.0 + ), + Map.of( + "studentId", studentId, + "semester", semester, + "courseName", "计算机网络", + "grade", 88.5 + ), + Map.of( + "studentId", studentId, + "semester", semester, + "courseName", "软件工程", + "grade", 90.0 + ) + ); + } +} diff --git a/backend/src/main/java/com/yoyuzh/cqu/Grade.java b/backend/src/main/java/com/yoyuzh/cqu/Grade.java new file mode 100644 index 0000000..63be796 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/cqu/Grade.java @@ -0,0 +1,110 @@ +package com.yoyuzh.cqu; + +import com.yoyuzh.auth.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "portal_grade", indexes = { + @Index(name = "idx_grade_user_semester", columnList = "user_id,semester,student_id"), + @Index(name = "idx_grade_user_created", columnList = "user_id,created_at") +}) +public class Grade { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "course_name", nullable = false, length = 255) + private String courseName; + + @Column(nullable = false) + private Double grade; + + @Column(nullable = false, length = 64) + private String semester; + + @Column(name = "student_id", length = 64) + private String studentId; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @PrePersist + public void prePersist() { + if (createdAt == null) { + createdAt = LocalDateTime.now(); + } + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public String getCourseName() { + return courseName; + } + + public void setCourseName(String courseName) { + this.courseName = courseName; + } + + public Double getGrade() { + return grade; + } + + public void setGrade(Double grade) { + this.grade = grade; + } + + public String getSemester() { + return semester; + } + + public void setSemester(String semester) { + this.semester = semester; + } + + public String getStudentId() { + return studentId; + } + + public void setStudentId(String studentId) { + this.studentId = studentId; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } +} diff --git a/backend/src/main/java/com/yoyuzh/cqu/GradeRepository.java b/backend/src/main/java/com/yoyuzh/cqu/GradeRepository.java new file mode 100644 index 0000000..6314a7e --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/cqu/GradeRepository.java @@ -0,0 +1,11 @@ +package com.yoyuzh.cqu; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface GradeRepository extends JpaRepository { + List findByUserIdAndStudentIdOrderBySemesterAscGradeDesc(Long userId, String studentId); + + void deleteByUserIdAndStudentIdAndSemester(Long userId, String studentId, String semester); +} diff --git a/backend/src/main/java/com/yoyuzh/cqu/GradeResponse.java b/backend/src/main/java/com/yoyuzh/cqu/GradeResponse.java new file mode 100644 index 0000000..76ac678 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/cqu/GradeResponse.java @@ -0,0 +1,8 @@ +package com.yoyuzh.cqu; + +public record GradeResponse( + String courseName, + Double grade, + String semester +) { +} diff --git a/backend/src/main/java/com/yoyuzh/files/FileController.java b/backend/src/main/java/com/yoyuzh/files/FileController.java new file mode 100644 index 0000000..21b0d1b --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/files/FileController.java @@ -0,0 +1,77 @@ +package com.yoyuzh.files; + +import com.yoyuzh.auth.CustomUserDetailsService; +import com.yoyuzh.common.ApiResponse; +import com.yoyuzh.common.PageResponse; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@RestController +@RequestMapping("/api/files") +@RequiredArgsConstructor +public class FileController { + + private final FileService fileService; + private final CustomUserDetailsService userDetailsService; + + @Operation(summary = "上传文件") + @PostMapping("/upload") + public ApiResponse upload(@AuthenticationPrincipal UserDetails userDetails, + @RequestParam(defaultValue = "/") String path, + @RequestPart("file") MultipartFile file) { + return ApiResponse.success(fileService.upload(userDetailsService.loadDomainUser(userDetails.getUsername()), path, file)); + } + + @Operation(summary = "创建目录") + @PostMapping("/mkdir") + public ApiResponse mkdir(@AuthenticationPrincipal UserDetails userDetails, + @Valid @ModelAttribute MkdirRequest request) { + return ApiResponse.success(fileService.mkdir(userDetailsService.loadDomainUser(userDetails.getUsername()), request.path())); + } + + @Operation(summary = "分页列出文件") + @GetMapping("/list") + public ApiResponse> list(@AuthenticationPrincipal UserDetails userDetails, + @RequestParam(defaultValue = "/") String path, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + return ApiResponse.success(fileService.list(userDetailsService.loadDomainUser(userDetails.getUsername()), path, page, size)); + } + + @Operation(summary = "最近文件") + @GetMapping("/recent") + public ApiResponse> recent(@AuthenticationPrincipal UserDetails userDetails) { + return ApiResponse.success(fileService.recent(userDetailsService.loadDomainUser(userDetails.getUsername()))); + } + + @Operation(summary = "下载文件") + @GetMapping("/download/{fileId}") + public ResponseEntity download(@AuthenticationPrincipal UserDetails userDetails, + @PathVariable Long fileId) { + return fileService.download(userDetailsService.loadDomainUser(userDetails.getUsername()), fileId); + } + + @Operation(summary = "删除文件") + @DeleteMapping("/{fileId}") + public ApiResponse delete(@AuthenticationPrincipal UserDetails userDetails, + @PathVariable Long fileId) { + fileService.delete(userDetailsService.loadDomainUser(userDetails.getUsername()), fileId); + return ApiResponse.success(); + } +} diff --git a/backend/src/main/java/com/yoyuzh/files/FileMetadataResponse.java b/backend/src/main/java/com/yoyuzh/files/FileMetadataResponse.java new file mode 100644 index 0000000..85ba4fb --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/files/FileMetadataResponse.java @@ -0,0 +1,14 @@ +package com.yoyuzh.files; + +import java.time.LocalDateTime; + +public record FileMetadataResponse( + Long id, + String filename, + String path, + long size, + String contentType, + boolean directory, + LocalDateTime createdAt +) { +} diff --git a/backend/src/main/java/com/yoyuzh/files/FileService.java b/backend/src/main/java/com/yoyuzh/files/FileService.java new file mode 100644 index 0000000..4cc4ced --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/files/FileService.java @@ -0,0 +1,216 @@ +package com.yoyuzh.files; + +import com.yoyuzh.auth.User; +import com.yoyuzh.common.BusinessException; +import com.yoyuzh.common.ErrorCode; +import com.yoyuzh.common.PageResponse; +import com.yoyuzh.config.FileStorageProperties; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.List; + +@Service +public class FileService { + + private final StoredFileRepository storedFileRepository; + private final Path rootPath; + private final long maxFileSize; + + public FileService(StoredFileRepository storedFileRepository, FileStorageProperties properties) { + this.storedFileRepository = storedFileRepository; + this.rootPath = Path.of(properties.getRootDir()).toAbsolutePath().normalize(); + this.maxFileSize = properties.getMaxFileSize(); + try { + Files.createDirectories(rootPath); + } catch (IOException ex) { + throw new IllegalStateException("无法初始化存储目录", ex); + } + } + + @Transactional + public FileMetadataResponse upload(User user, String path, MultipartFile multipartFile) { + String normalizedPath = normalizeDirectoryPath(path); + String filename = StringUtils.cleanPath(multipartFile.getOriginalFilename()); + if (!StringUtils.hasText(filename)) { + throw new BusinessException(ErrorCode.UNKNOWN, "文件名不能为空"); + } + if (multipartFile.getSize() > maxFileSize) { + throw new BusinessException(ErrorCode.UNKNOWN, "文件大小超出限制"); + } + if (storedFileRepository.existsByUserIdAndPathAndFilename(user.getId(), normalizedPath, filename)) { + throw new BusinessException(ErrorCode.UNKNOWN, "同目录下文件已存在"); + } + + Path targetDir = resolveUserPath(user.getId(), normalizedPath); + Path targetFile = targetDir.resolve(filename).normalize(); + try { + Files.createDirectories(targetDir); + Files.copy(multipartFile.getInputStream(), targetFile, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException ex) { + throw new BusinessException(ErrorCode.UNKNOWN, "文件上传失败"); + } + + StoredFile storedFile = new StoredFile(); + storedFile.setUser(user); + storedFile.setFilename(filename); + storedFile.setPath(normalizedPath); + storedFile.setStorageName(filename); + storedFile.setContentType(multipartFile.getContentType()); + storedFile.setSize(multipartFile.getSize()); + storedFile.setDirectory(false); + return toResponse(storedFileRepository.save(storedFile)); + } + + @Transactional + public FileMetadataResponse mkdir(User user, String path) { + String normalizedPath = normalizeDirectoryPath(path); + if ("/".equals(normalizedPath)) { + throw new BusinessException(ErrorCode.UNKNOWN, "根目录无需创建"); + } + String parentPath = extractParentPath(normalizedPath); + String directoryName = extractLeafName(normalizedPath); + if (storedFileRepository.existsByUserIdAndPathAndFilename(user.getId(), parentPath, directoryName)) { + throw new BusinessException(ErrorCode.UNKNOWN, "目录已存在"); + } + try { + Files.createDirectories(resolveUserPath(user.getId(), normalizedPath)); + } catch (IOException ex) { + throw new BusinessException(ErrorCode.UNKNOWN, "目录创建失败"); + } + + StoredFile storedFile = new StoredFile(); + storedFile.setUser(user); + storedFile.setFilename(directoryName); + storedFile.setPath(parentPath); + storedFile.setStorageName(directoryName); + storedFile.setContentType("directory"); + storedFile.setSize(0L); + storedFile.setDirectory(true); + return toResponse(storedFileRepository.save(storedFile)); + } + + public PageResponse list(User user, String path, int page, int size) { + String normalizedPath = normalizeDirectoryPath(path); + Page result = storedFileRepository.findByUserIdAndPathOrderByDirectoryDescCreatedAtDesc( + user.getId(), normalizedPath, PageRequest.of(page, size)); + List items = result.getContent().stream().map(this::toResponse).toList(); + return new PageResponse<>(items, result.getTotalElements(), page, size); + } + + public List recent(User user) { + return storedFileRepository.findTop12ByUserIdAndDirectoryFalseOrderByCreatedAtDesc(user.getId()) + .stream() + .map(this::toResponse) + .toList(); + } + + @Transactional + public void delete(User user, Long fileId) { + StoredFile storedFile = storedFileRepository.findById(fileId) + .orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "文件不存在")); + if (!storedFile.getUser().getId().equals(user.getId())) { + throw new BusinessException(ErrorCode.PERMISSION_DENIED, "没有权限删除该文件"); + } + try { + Path basePath = resolveUserPath(user.getId(), storedFile.getPath()); + Path target = storedFile.isDirectory() + ? basePath.resolve(storedFile.getFilename()).normalize() + : basePath.resolve(storedFile.getStorageName()).normalize(); + Files.deleteIfExists(target); + } catch (IOException ex) { + throw new BusinessException(ErrorCode.UNKNOWN, "删除文件失败"); + } + storedFileRepository.delete(storedFile); + } + + public ResponseEntity download(User user, Long fileId) { + StoredFile storedFile = storedFileRepository.findById(fileId) + .orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "文件不存在")); + if (!storedFile.getUser().getId().equals(user.getId())) { + throw new BusinessException(ErrorCode.PERMISSION_DENIED, "没有权限下载该文件"); + } + if (storedFile.isDirectory()) { + throw new BusinessException(ErrorCode.UNKNOWN, "目录不支持下载"); + } + try { + Path filePath = resolveUserPath(user.getId(), storedFile.getPath()).resolve(storedFile.getStorageName()).normalize(); + byte[] body = Files.readAllBytes(filePath); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename*=UTF-8''" + URLEncoder.encode(storedFile.getFilename(), StandardCharsets.UTF_8)) + .contentType(MediaType.parseMediaType( + storedFile.getContentType() == null ? MediaType.APPLICATION_OCTET_STREAM_VALUE : storedFile.getContentType())) + .body(body); + } catch (IOException ex) { + throw new BusinessException(ErrorCode.FILE_NOT_FOUND, "文件不存在"); + } + } + + private FileMetadataResponse toResponse(StoredFile storedFile) { + String logicalPath = storedFile.getPath(); + if (storedFile.isDirectory()) { + logicalPath = "/".equals(storedFile.getPath()) + ? "/" + storedFile.getFilename() + : storedFile.getPath() + "/" + storedFile.getFilename(); + } + return new FileMetadataResponse( + storedFile.getId(), + storedFile.getFilename(), + logicalPath, + storedFile.getSize(), + storedFile.getContentType(), + storedFile.isDirectory(), + storedFile.getCreatedAt()); + } + + private String normalizeDirectoryPath(String path) { + if (!StringUtils.hasText(path) || "/".equals(path.trim())) { + return "/"; + } + String normalized = path.replace("\\", "/").trim(); + if (!normalized.startsWith("/")) { + normalized = "/" + normalized; + } + normalized = normalized.replaceAll("/{2,}", "/"); + if (normalized.contains("..")) { + throw new BusinessException(ErrorCode.UNKNOWN, "路径不合法"); + } + if (normalized.endsWith("/") && normalized.length() > 1) { + normalized = normalized.substring(0, normalized.length() - 1); + } + return normalized; + } + + private Path resolveUserPath(Long userId, String normalizedPath) { + Path userRoot = rootPath.resolve(userId.toString()).normalize(); + Path relative = "/".equals(normalizedPath) ? Path.of("") : Path.of(normalizedPath.substring(1)); + Path resolved = userRoot.resolve(relative).normalize(); + if (!resolved.startsWith(userRoot)) { + throw new BusinessException(ErrorCode.UNKNOWN, "路径不合法"); + } + return resolved; + } + + private String extractParentPath(String normalizedPath) { + int lastSlash = normalizedPath.lastIndexOf('/'); + return lastSlash <= 0 ? "/" : normalizedPath.substring(0, lastSlash); + } + + private String extractLeafName(String normalizedPath) { + return normalizedPath.substring(normalizedPath.lastIndexOf('/') + 1); + } +} diff --git a/backend/src/main/java/com/yoyuzh/files/MkdirRequest.java b/backend/src/main/java/com/yoyuzh/files/MkdirRequest.java new file mode 100644 index 0000000..cd75952 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/files/MkdirRequest.java @@ -0,0 +1,6 @@ +package com.yoyuzh.files; + +import jakarta.validation.constraints.NotBlank; + +public record MkdirRequest(@NotBlank String path) { +} diff --git a/backend/src/main/java/com/yoyuzh/files/StoredFile.java b/backend/src/main/java/com/yoyuzh/files/StoredFile.java new file mode 100644 index 0000000..705da05 --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/files/StoredFile.java @@ -0,0 +1,132 @@ +package com.yoyuzh.files; + +import com.yoyuzh.auth.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "portal_file", indexes = { + @Index(name = "uk_file_user_path_name", columnList = "user_id,path,filename", unique = true), + @Index(name = "idx_file_created_at", columnList = "created_at") +}) +public class StoredFile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false, length = 255) + private String filename; + + @Column(nullable = false, length = 512) + private String path; + + @Column(name = "storage_name", nullable = false, length = 255) + private String storageName; + + @Column(name = "content_type", length = 255) + private String contentType; + + @Column(nullable = false) + private Long size; + + @Column(name = "is_directory", nullable = false) + private boolean directory; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @PrePersist + public void prePersist() { + if (createdAt == null) { + createdAt = LocalDateTime.now(); + } + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public String getFilename() { + return filename; + } + + public void setFilename(String filename) { + this.filename = filename; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getStorageName() { + return storageName; + } + + public void setStorageName(String storageName) { + this.storageName = storageName; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public Long getSize() { + return size; + } + + public void setSize(Long size) { + this.size = size; + } + + public boolean isDirectory() { + return directory; + } + + public void setDirectory(boolean directory) { + this.directory = directory; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } +} diff --git a/backend/src/main/java/com/yoyuzh/files/StoredFileRepository.java b/backend/src/main/java/com/yoyuzh/files/StoredFileRepository.java new file mode 100644 index 0000000..dc19b4c --- /dev/null +++ b/backend/src/main/java/com/yoyuzh/files/StoredFileRepository.java @@ -0,0 +1,32 @@ +package com.yoyuzh.files; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface StoredFileRepository extends JpaRepository { + + @Query(""" + select case when count(f) > 0 then true else false end + from StoredFile f + where f.user.id = :userId and f.path = :path and f.filename = :filename + """) + boolean existsByUserIdAndPathAndFilename(@Param("userId") Long userId, + @Param("path") String path, + @Param("filename") String filename); + + @Query(""" + select f from StoredFile f + where f.user.id = :userId and f.path = :path + order by f.directory desc, f.createdAt desc + """) + Page findByUserIdAndPathOrderByDirectoryDescCreatedAtDesc(@Param("userId") Long userId, + @Param("path") String path, + Pageable pageable); + + List findTop12ByUserIdAndDirectoryFalseOrderByCreatedAtDesc(Long userId); +} diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml new file mode 100644 index 0000000..cbd5e5b --- /dev/null +++ b/backend/src/main/resources/application-dev.yml @@ -0,0 +1,17 @@ +spring: + datasource: + url: jdbc:h2:file:./data/yoyuzh_portal_dev;MODE=MySQL;AUTO_SERVER=TRUE;DB_CLOSE_DELAY=-1 + username: sa + password: + driver-class-name: org.h2.Driver + jpa: + hibernate: + ddl-auto: update + h2: + console: + enabled: true + path: /h2-console + +app: + cqu: + mock-enabled: true diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..67c9934 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,41 @@ +server: + port: 8080 + +spring: + application: + name: yoyuzh-portal-backend + datasource: + url: jdbc:mysql://localhost:3306/yoyuzh_portal?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: update + open-in-view: false + properties: + hibernate: + format_sql: true + servlet: + multipart: + max-file-size: 50MB + max-request-size: 50MB + +app: + jwt: + secret: change-me-change-me-change-me-change-me + expiration-seconds: 86400 + storage: + root-dir: ./storage + max-file-size: 52428800 + cqu: + base-url: https://example-cqu-api.local + require-login: false + mock-enabled: false + +springdoc: + swagger-ui: + path: /swagger-ui.html + +logging: + config: classpath:logback.xml diff --git a/backend/src/main/resources/logback.xml b/backend/src/main/resources/logback.xml new file mode 100644 index 0000000..bccb197 --- /dev/null +++ b/backend/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + + ${CONSOLE_PATTERN} + + + + + + + diff --git a/backend/src/test/java/com/yoyuzh/auth/AuthServiceTest.java b/backend/src/test/java/com/yoyuzh/auth/AuthServiceTest.java new file mode 100644 index 0000000..d54392b --- /dev/null +++ b/backend/src/test/java/com/yoyuzh/auth/AuthServiceTest.java @@ -0,0 +1,105 @@ +package com.yoyuzh.auth; + +import com.yoyuzh.auth.dto.AuthResponse; +import com.yoyuzh.auth.dto.LoginRequest; +import com.yoyuzh.auth.dto.RegisterRequest; +import com.yoyuzh.common.BusinessException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AuthServiceTest { + + @Mock + private UserRepository userRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @Mock + private AuthenticationManager authenticationManager; + + @Mock + private JwtTokenProvider jwtTokenProvider; + + @InjectMocks + private AuthService authService; + + @Test + void shouldRegisterUserWithEncryptedPassword() { + RegisterRequest request = new RegisterRequest("alice", "alice@example.com", "plain-password"); + when(userRepository.existsByUsername("alice")).thenReturn(false); + when(userRepository.existsByEmail("alice@example.com")).thenReturn(false); + when(passwordEncoder.encode("plain-password")).thenReturn("encoded-password"); + when(userRepository.save(any(User.class))).thenAnswer(invocation -> { + User user = invocation.getArgument(0); + user.setId(1L); + user.setCreatedAt(LocalDateTime.now()); + return user; + }); + when(jwtTokenProvider.generateToken(1L, "alice")).thenReturn("jwt-token"); + + AuthResponse response = authService.register(request); + + assertThat(response.token()).isEqualTo("jwt-token"); + assertThat(response.user().username()).isEqualTo("alice"); + verify(passwordEncoder).encode("plain-password"); + } + + @Test + void shouldRejectDuplicateUsernameOnRegister() { + RegisterRequest request = new RegisterRequest("alice", "alice@example.com", "plain-password"); + when(userRepository.existsByUsername("alice")).thenReturn(true); + + assertThatThrownBy(() -> authService.register(request)) + .isInstanceOf(BusinessException.class) + .hasMessageContaining("用户名已存在"); + } + + @Test + void shouldLoginAndReturnToken() { + LoginRequest request = new LoginRequest("alice", "plain-password"); + User user = new User(); + user.setId(1L); + user.setUsername("alice"); + user.setEmail("alice@example.com"); + user.setPasswordHash("encoded-password"); + user.setCreatedAt(LocalDateTime.now()); + when(userRepository.findByUsername("alice")).thenReturn(Optional.of(user)); + when(jwtTokenProvider.generateToken(1L, "alice")).thenReturn("jwt-token"); + + AuthResponse response = authService.login(request); + + verify(authenticationManager).authenticate( + new UsernamePasswordAuthenticationToken("alice", "plain-password")); + assertThat(response.token()).isEqualTo("jwt-token"); + assertThat(response.user().email()).isEqualTo("alice@example.com"); + } + + @Test + void shouldThrowBusinessExceptionWhenAuthenticationFails() { + LoginRequest request = new LoginRequest("alice", "wrong-password"); + when(authenticationManager.authenticate(any())) + .thenThrow(new BadCredentialsException("bad credentials")); + + assertThatThrownBy(() -> authService.login(request)) + .isInstanceOf(BusinessException.class) + .hasMessageContaining("用户名或密码错误"); + } +} diff --git a/backend/src/test/java/com/yoyuzh/cqu/CquDataServiceTest.java b/backend/src/test/java/com/yoyuzh/cqu/CquDataServiceTest.java new file mode 100644 index 0000000..37b56ad --- /dev/null +++ b/backend/src/test/java/com/yoyuzh/cqu/CquDataServiceTest.java @@ -0,0 +1,86 @@ +package com.yoyuzh.cqu; + +import com.yoyuzh.auth.User; +import com.yoyuzh.config.CquApiProperties; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CquDataServiceTest { + + @Mock + private CquApiClient cquApiClient; + + @Mock + private CourseRepository courseRepository; + + @Mock + private GradeRepository gradeRepository; + + @InjectMocks + private CquDataService cquDataService; + + @Test + void shouldNormalizeScheduleFromRemoteApi() { + CquApiProperties properties = new CquApiProperties(); + properties.setRequireLogin(false); + cquDataService = new CquDataService(cquApiClient, courseRepository, gradeRepository, properties); + when(cquApiClient.fetchSchedule("2025-2026-1", "20230001")).thenReturn(List.of(Map.of( + "courseName", "Java", + "teacher", "Zhang", + "classroom", "A101", + "dayOfWeek", 1, + "startTime", 1, + "endTime", 2 + ))); + + List response = cquDataService.getSchedule(null, "2025-2026-1", "20230001"); + + assertThat(response).hasSize(1); + assertThat(response.get(0).courseName()).isEqualTo("Java"); + assertThat(response.get(0).teacher()).isEqualTo("Zhang"); + } + + @Test + void shouldPersistGradesForLoggedInUserWhenAvailable() { + CquApiProperties properties = new CquApiProperties(); + properties.setRequireLogin(true); + cquDataService = new CquDataService(cquApiClient, courseRepository, gradeRepository, properties); + User user = new User(); + user.setId(1L); + user.setUsername("alice"); + user.setEmail("alice@example.com"); + user.setPasswordHash("encoded"); + user.setCreatedAt(LocalDateTime.now()); + when(cquApiClient.fetchGrades("2025-2026-1", "20230001")).thenReturn(List.of(Map.of( + "courseName", "Java", + "grade", 95, + "semester", "2025-2026-1" + ))); + Grade persisted = new Grade(); + persisted.setUser(user); + persisted.setCourseName("Java"); + persisted.setGrade(95D); + persisted.setSemester("2025-2026-1"); + persisted.setStudentId("20230001"); + when(gradeRepository.saveAll(anyList())).thenReturn(List.of(persisted)); + when(gradeRepository.findByUserIdAndStudentIdOrderBySemesterAscGradeDesc(1L, "20230001")) + .thenReturn(List.of(persisted)); + + List response = cquDataService.getGrades(user, "2025-2026-1", "20230001"); + + assertThat(response).hasSize(1); + assertThat(response.get(0).grade()).isEqualTo(95D); + } +} diff --git a/backend/src/test/java/com/yoyuzh/cqu/CquMockDataFactoryTest.java b/backend/src/test/java/com/yoyuzh/cqu/CquMockDataFactoryTest.java new file mode 100644 index 0000000..d79e8d2 --- /dev/null +++ b/backend/src/test/java/com/yoyuzh/cqu/CquMockDataFactoryTest.java @@ -0,0 +1,29 @@ +package com.yoyuzh.cqu; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class CquMockDataFactoryTest { + + @Test + void shouldCreateMockScheduleForStudentAndSemester() { + List> result = CquMockDataFactory.createSchedule("2025-2026-1", "20230001"); + + assertThat(result).isNotEmpty(); + assertThat(result.get(0)).containsEntry("courseName", "高级 Java 程序设计"); + assertThat(result.get(0)).containsEntry("semester", "2025-2026-1"); + } + + @Test + void shouldCreateMockGradesForStudentAndSemester() { + List> result = CquMockDataFactory.createGrades("2025-2026-1", "20230001"); + + assertThat(result).isNotEmpty(); + assertThat(result.get(0)).containsEntry("studentId", "20230001"); + assertThat(result.get(0)).containsKey("grade"); + } +} diff --git a/backend/src/test/java/com/yoyuzh/files/FileServiceTest.java b/backend/src/test/java/com/yoyuzh/files/FileServiceTest.java new file mode 100644 index 0000000..5088301 --- /dev/null +++ b/backend/src/test/java/com/yoyuzh/files/FileServiceTest.java @@ -0,0 +1,114 @@ +package com.yoyuzh.files; + +import com.yoyuzh.auth.User; +import com.yoyuzh.common.BusinessException; +import com.yoyuzh.config.FileStorageProperties; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.mock.web.MockMultipartFile; + +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class FileServiceTest { + + @Mock + private StoredFileRepository storedFileRepository; + + private FileService fileService; + + @TempDir + Path tempDir; + + @BeforeEach + void setUp() { + FileStorageProperties properties = new FileStorageProperties(); + properties.setRootDir(tempDir.toString()); + properties.setMaxFileSize(50 * 1024 * 1024); + fileService = new FileService(storedFileRepository, properties); + } + + @Test + void shouldStoreUploadedFileUnderUserDirectory() { + User user = createUser(7L); + MockMultipartFile multipartFile = new MockMultipartFile( + "file", "notes.txt", "text/plain", "hello".getBytes()); + when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "notes.txt")).thenReturn(false); + when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> { + StoredFile file = invocation.getArgument(0); + file.setId(10L); + return file; + }); + + FileMetadataResponse response = fileService.upload(user, "/docs", multipartFile); + + assertThat(response.id()).isEqualTo(10L); + assertThat(response.path()).isEqualTo("/docs"); + assertThat(response.directory()).isFalse(); + assertThat(tempDir.resolve("7/docs/notes.txt")).exists(); + } + + @Test + void shouldRejectDeletingOtherUsersFile() { + User owner = createUser(1L); + User requester = createUser(2L); + StoredFile storedFile = createFile(100L, owner, "/docs", "notes.txt"); + when(storedFileRepository.findById(100L)).thenReturn(Optional.of(storedFile)); + + assertThatThrownBy(() -> fileService.delete(requester, 100L)) + .isInstanceOf(BusinessException.class) + .hasMessageContaining("没有权限"); + } + + @Test + void shouldListFilesByPathWithPagination() { + User user = createUser(7L); + StoredFile file = createFile(100L, user, "/docs", "notes.txt"); + when(storedFileRepository.findByUserIdAndPathOrderByDirectoryDescCreatedAtDesc( + 7L, "/docs", PageRequest.of(0, 10))) + .thenReturn(new PageImpl<>(List.of(file))); + + var result = fileService.list(user, "/docs", 0, 10); + + assertThat(result.items()).hasSize(1); + assertThat(result.items().get(0).filename()).isEqualTo("notes.txt"); + } + + private User createUser(Long id) { + User user = new User(); + user.setId(id); + user.setUsername("user-" + id); + user.setEmail("user-" + id + "@example.com"); + user.setPasswordHash("encoded"); + user.setCreatedAt(LocalDateTime.now()); + return user; + } + + private StoredFile createFile(Long id, User user, String path, String filename) { + StoredFile file = new StoredFile(); + file.setId(id); + file.setUser(user); + file.setFilename(filename); + file.setPath(path); + file.setSize(5L); + file.setDirectory(false); + file.setStorageName(filename); + file.setCreatedAt(LocalDateTime.now()); + return file; + } +} diff --git a/front/.env.example b/front/.env.example new file mode 100644 index 0000000..7a550fe --- /dev/null +++ b/front/.env.example @@ -0,0 +1,9 @@ +# GEMINI_API_KEY: Required for Gemini AI API calls. +# AI Studio automatically injects this at runtime from user secrets. +# Users configure this via the Secrets panel in the AI Studio UI. +GEMINI_API_KEY="MY_GEMINI_API_KEY" + +# APP_URL: The URL where this applet is hosted. +# AI Studio automatically injects this at runtime with the Cloud Run service URL. +# Used for self-referential links, OAuth callbacks, and API endpoints. +APP_URL="MY_APP_URL" diff --git a/front/.gitignore b/front/.gitignore new file mode 100644 index 0000000..5a86d2a --- /dev/null +++ b/front/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +build/ +dist/ +coverage/ +.DS_Store +*.log +.env* +!.env.example diff --git a/front/.vite/deps/_metadata.json b/front/.vite/deps/_metadata.json new file mode 100644 index 0000000..60b90a3 --- /dev/null +++ b/front/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "1eac4ae6", + "configHash": "19e214db", + "lockfileHash": "126cd023", + "browserHash": "c5ddb224", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/front/.vite/deps/package.json b/front/.vite/deps/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/front/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/front/README.md b/front/README.md new file mode 100644 index 0000000..245a288 --- /dev/null +++ b/front/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/7dcdc5c7-28c0-4121-959b-77273973e0ef + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/vue/index.html b/front/index.html similarity index 52% rename from vue/index.html rename to front/index.html index 8a15401..21dfe69 100644 --- a/vue/index.html +++ b/front/index.html @@ -2,12 +2,12 @@ - - test1 + My Google AI Studio App -
- +
+ + diff --git a/front/metadata.json b/front/metadata.json new file mode 100644 index 0000000..c2590a5 --- /dev/null +++ b/front/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Personal Portal", + "description": "A unified personal portal for managing files, school schedules, grades, and games with a glassmorphism design.", + "requestFramePermissions": [] +} diff --git a/front/package-lock.json b/front/package-lock.json new file mode 100644 index 0000000..bb79d48 --- /dev/null +++ b/front/package-lock.json @@ -0,0 +1,5281 @@ +{ + "name": "react-example", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "react-example", + "version": "0.0.0", + "dependencies": { + "@google/genai": "^1.29.0", + "@tailwindcss/vite": "^4.1.14", + "@vitejs/plugin-react": "^5.0.4", + "better-sqlite3": "^12.4.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "dotenv": "^17.2.3", + "express": "^4.21.2", + "lucide-react": "^0.546.0", + "motion": "^12.23.24", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.13.1", + "tailwind-merge": "^3.5.0", + "vite": "^6.2.0" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.14.0", + "autoprefixer": "^10.4.21", + "tailwindcss": "^4.1.14", + "tsx": "^4.21.0", + "typescript": "~5.8.2", + "vite": "^6.2.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/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-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-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-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "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/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.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/@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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.44.0.tgz", + "integrity": "sha512-kRt9ZtuXmz+tLlcNntN/VV4LRdpl6ZOu5B1KbfNgfR65db15O6sUQcwnwLka8sT/V6qysD93fWrgJHF2L7dA9A==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.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/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "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/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "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/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "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/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "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/better-sqlite3": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz", + "integrity": "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "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==", + "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/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "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==", + "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/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "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/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "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/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "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==", + "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==", + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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==", + "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/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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==", + "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": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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==", + "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==", + "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==", + "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==", + "devOptional": 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==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "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/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.35.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.35.2.tgz", + "integrity": "sha512-dhfuEMaNo0hc+AEqyHiIfiJRNb9U9UQutE9FoKm5pjf7CMitp9xPEF1iWZihR1q86LBmo6EJ7S8cN8QXEy49AA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.35.2", + "motion-utils": "^12.29.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "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", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.1.tgz", + "integrity": "sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "7.1.3", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "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==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "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/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==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "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/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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/lucide-react": { + "version": "0.546.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.546.0.tgz", + "integrity": "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "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==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/motion": { + "version": "12.35.2", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.35.2.tgz", + "integrity": "sha512-8zCi1DkNyU6a/tgEHn/GnnXZDcaMpDHbDOGORY1Rg/6lcNMSOuvwDB3i4hMSOvxqMWArc/vrGaw/Xek1OP69/A==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.35.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.35.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.35.2.tgz", + "integrity": "sha512-pWXFMTwvGDbx1Fe9YL5HZebv2NhvGBzRtiNUv58aoK7+XrsuaydQ0JGRKK2r+bTKlwgSWwWxHbP5249Qr/BNpg==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.29.2" + } + }, + "node_modules/motion-utils": { + "version": "12.29.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", + "license": "MIT" + }, + "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/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.88.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.88.0.tgz", + "integrity": "sha512-At6b4UqIEVudaqPsXjmUO1r/N5BUr4yhDGs5PkBE8/oG5+TfLPhFechiskFsnT6Ql0VfUXbalUUCbfXxtj7K+w==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/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/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "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/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "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==", + "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==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/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==", + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "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/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "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/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "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/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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "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/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "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/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4.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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "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/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "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-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "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==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "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/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + } + } +} diff --git a/front/package.json b/front/package.json new file mode 100644 index 0000000..6dedf4c --- /dev/null +++ b/front/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port=3000 --host=0.0.0.0", + "build": "vite build", + "preview": "vite preview", + "clean": "rm -rf dist", + "lint": "tsc --noEmit" + }, + "dependencies": { + "@google/genai": "^1.29.0", + "@tailwindcss/vite": "^4.1.14", + "@vitejs/plugin-react": "^5.0.4", + "better-sqlite3": "^12.4.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "dotenv": "^17.2.3", + "express": "^4.21.2", + "lucide-react": "^0.546.0", + "motion": "^12.23.24", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.13.1", + "tailwind-merge": "^3.5.0", + "vite": "^6.2.0" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.14.0", + "autoprefixer": "^10.4.21", + "tailwindcss": "^4.1.14", + "tsx": "^4.21.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} diff --git a/front/src/App.tsx b/front/src/App.tsx new file mode 100644 index 0000000..ecbf184 --- /dev/null +++ b/front/src/App.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { Layout } from './components/layout/Layout'; +import Login from './pages/Login'; +import Overview from './pages/Overview'; +import Files from './pages/Files'; +import School from './pages/School'; +import Games from './pages/Games'; + +export default function App() { + return ( + + + } /> + }> + } /> + } /> + } /> + } /> + } /> + + + + ); +} diff --git a/front/src/components/layout/Layout.tsx b/front/src/components/layout/Layout.tsx new file mode 100644 index 0000000..1cea544 --- /dev/null +++ b/front/src/components/layout/Layout.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { NavLink, Outlet, useNavigate } from 'react-router-dom'; +import { cn } from '@/src/lib/utils'; +import { LayoutDashboard, FolderOpen, GraduationCap, Gamepad2, LogOut } from 'lucide-react'; + +const NAV_ITEMS = [ + { name: '总览', path: '/overview', icon: LayoutDashboard }, + { name: '网盘', path: '/files', icon: FolderOpen }, + { name: '教务', path: '/school', icon: GraduationCap }, + { name: '游戏', path: '/games', icon: Gamepad2 }, +]; + +export function Layout() { + const navigate = useNavigate(); + + const handleLogout = () => { + navigate('/login'); + }; + + return ( +
+ {/* Animated Gradient Background */} +
+
+
+
+
+ + {/* Top Navigation */} +
+
+ {/* Brand */} +
+
+ Y +
+
+ YOYUZH.XYZ + Personal Portal +
+
+ + {/* Nav Links */} + + + {/* User / Actions */} +
+ +
+
+
+ + {/* Main Content */} +
+ +
+
+ ); +} + diff --git a/front/src/components/ui/button.tsx b/front/src/components/ui/button.tsx new file mode 100644 index 0000000..4d50f99 --- /dev/null +++ b/front/src/components/ui/button.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cn } from "@/src/lib/utils" + +export interface ButtonProps + extends React.ButtonHTMLAttributes { + variant?: "default" | "outline" | "ghost" | "glass" + size?: "default" | "sm" | "lg" | "icon" +} + +const Button = React.forwardRef( + ({ className, variant = "default", size = "default", ...props }, ref) => { + return ( + + ))} +
+ +
+

网盘目录

+ {DIRECTORIES.map((item) => ( + + ))} +
+ + + + {/* Middle Content */} + + {/* Header / Breadcrumbs */} +
+
+ + {currentPath.map((pathItem, index) => ( + + + + + ))} +
+
+ + +
+
+ + {/* File List */} +
+ + + + + + + + + + + + {currentFiles.length > 0 ? ( + currentFiles.map((file) => ( + setSelectedFile(file)} + onDoubleClick={() => handleFolderDoubleClick(file)} + className={cn( + "group cursor-pointer transition-colors border-b border-white/5 last:border-0", + selectedFile?.id === file.id ? "bg-[#336EFF]/10" : "hover:bg-white/[0.02]" + )} + > + + + + + + + )) + ) : ( + + + + )} + +
名称修改日期类型大小
+
+ {file.type === 'folder' ? ( + + ) : file.type === 'image' ? ( + + ) : ( + + )} + + {file.name} + +
+
{file.modified}{file.type}{file.size} + +
+
+ +

此文件夹为空

+
+
+
+ + {/* Bottom Actions */} +
+ + +
+
+ + {/* Right Sidebar (Details) */} + {selectedFile && ( + + + + 详细信息 + + +
+
+ {selectedFile.type === 'folder' ? ( + + ) : selectedFile.type === 'image' ? ( + + ) : ( + + )} +
+

{selectedFile.name}

+
+ +
+ ${currentPath.join(' > ')}`} /> + + + +
+ + {selectedFile.type !== 'folder' && ( + + )} + {selectedFile.type === 'folder' && ( + + )} +
+
+
+ )} +
+ ); +} + +function DetailItem({ label, value }: { label: string, value: string }) { + return ( +
+

{label}

+

{value}

+
+ ); +} diff --git a/front/src/pages/Games.tsx b/front/src/pages/Games.tsx new file mode 100644 index 0000000..49d61c1 --- /dev/null +++ b/front/src/pages/Games.tsx @@ -0,0 +1,109 @@ +import React, { useState } from 'react'; +import { motion } from 'motion/react'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/src/components/ui/card'; +import { Button } from '@/src/components/ui/button'; +import { Gamepad2, Rocket, Cat, Car, Play } from 'lucide-react'; +import { cn } from '@/src/lib/utils'; + +const GAMES = [ + { + id: 'cat', + name: 'CAT', + description: '简单的小猫升级游戏,通过点击获取经验,解锁不同形态的猫咪。', + icon: Cat, + color: 'from-orange-400 to-red-500', + category: 'featured' + }, + { + id: 'race', + name: 'RACE', + description: '赛车休闲小游戏,躲避障碍物,挑战最高分记录。', + icon: Car, + color: 'from-blue-400 to-indigo-500', + category: 'featured' + } +]; + +export default function Games() { + const [activeTab, setActiveTab] = useState<'featured' | 'all'>('featured'); + + return ( +
+ {/* Hero Section */} + +
+
+
+ + Entertainment +
+

游戏入口

+

+ 保留轻量试玩与静态资源检查入口,维持与整站一致的毛玻璃语言。在这里您可以快速启动站内集成的小游戏。 +

+
+ + + {/* Category Tabs */} +
+ + +
+ + {/* Game Grid */} +
+ {GAMES.map((game, index) => ( + + +
+ +
+
+ +
+ + {game.category} + +
+ {game.name} + + {game.description} + +
+ + + + + + ))} +
+
+ ); +} diff --git a/front/src/pages/Login.tsx b/front/src/pages/Login.tsx new file mode 100644 index 0000000..38d772f --- /dev/null +++ b/front/src/pages/Login.tsx @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { motion } from 'motion/react'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/src/components/ui/card'; +import { Button } from '@/src/components/ui/button'; +import { Input } from '@/src/components/ui/input'; +import { LogIn, User, Lock } from 'lucide-react'; + +export default function Login() { + const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleLogin = (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + // Simulate login + setTimeout(() => { + setLoading(false); + navigate('/overview'); + }, 1000); + }; + + return ( +
+ {/* Background Glow */} +
+
+ +
+ {/* Left Side: Brand Info */} + +
+ + Access Portal +
+ +
+

YOYUZH.XYZ

+

+ 个人网站
统一入口 +

+
+ +

+ 欢迎来到 YOYUZH 的个人门户。在这里,你可以集中管理个人网盘文件、查询教务成绩课表,以及体验轻量级小游戏。 +

+
+ + {/* Right Side: Login Form */} + + + + + + 登录 + + + 请输入您的账号和密码以继续 + + + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + {error && ( +
+ {error} +
+ )} + + +
+
+
+
+
+
+ ); +} diff --git a/front/src/pages/Overview.tsx b/front/src/pages/Overview.tsx new file mode 100644 index 0000000..1eb9755 --- /dev/null +++ b/front/src/pages/Overview.tsx @@ -0,0 +1,208 @@ +import React from 'react'; +import { motion } from 'motion/react'; +import { useNavigate } from 'react-router-dom'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/src/components/ui/card'; +import { Button } from '@/src/components/ui/button'; +import { + FileText, Upload, FolderPlus, Database, + GraduationCap, BookOpen, Clock, HardDrive, + User, Mail, ChevronRight +} from 'lucide-react'; + +export default function Overview() { + const navigate = useNavigate(); + const currentHour = new Date().getHours(); + let greeting = '晚上好'; + if (currentHour < 6) greeting = '凌晨好'; + else if (currentHour < 12) greeting = '早上好'; + else if (currentHour < 18) greeting = '下午好'; + + const currentTime = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); + + return ( +
+ {/* Hero Section */} + +
+
+

欢迎回来,tester5595

+

现在时间 {currentTime} · {greeting}

+

+ 这是您的个人门户总览。在这里您可以快速查看网盘文件状态、近期课程安排以及教务成绩摘要。 +

+
+ + + {/* Metrics Cards */} +
+ + + + +
+ +
+ {/* Left Column */} +
+ {/* Recent Files */} + + + 最近文件 + + + +
+ {[ + { name: '软件工程期末复习资料.pdf', size: '2.4 MB', time: '2小时前' }, + { name: '2025春季学期课表.xlsx', size: '156 KB', time: '昨天 14:30' }, + { name: '项目架构设计图.png', size: '4.1 MB', time: '3天前' }, + ].map((file, i) => ( +
navigate('/files')}> +
+
+ +
+
+

{file.name}

+

{file.time}

+
+
+ {file.size} +
+ ))} +
+
+
+ + {/* Schedule */} + + + 今日 / 本周课程 +
+ + +
+
+ +
+ {[ + { time: '08:00 - 09:35', name: '高等数学 (下)', room: '教1-204' }, + { time: '10:00 - 11:35', name: '大学物理', room: '教2-101' }, + { time: '14:00 - 15:35', name: '软件工程', room: '计科楼 302' }, + ].map((course, i) => ( +
+
{course.time}
+
+

{course.name}

+

+ {course.room} +

+
+
+ ))} +
+
+
+
+ + {/* Right Column */} +
+ {/* Quick Actions */} + + + 快捷操作 + + +
+ navigate('/files')} /> + navigate('/files')} /> + navigate('/files')} /> + navigate('/school')} /> +
+
+
+ + {/* Storage */} + + + 存储空间 + + +
+
+

12.6 GB

+

已使用 / 共 50 GB

+
+ 25% +
+
+
+
+ + + + {/* Account Info */} + + + 账号信息 + + +
+
+ T +
+
+

tester5595

+

tester5595@example.com

+
+
+
+
+
+
+
+ ); +} + +function MetricCard({ title, value, desc, icon: Icon, delay }: any) { + return ( + + + +
+
+ +
+ {value} +
+
+

{title}

+

{desc}

+
+
+
+
+ ); +} + +function QuickAction({ icon: Icon, label, onClick }: any) { + return ( + + ); +} diff --git a/front/src/pages/School.tsx b/front/src/pages/School.tsx new file mode 100644 index 0000000..cb42b95 --- /dev/null +++ b/front/src/pages/School.tsx @@ -0,0 +1,291 @@ +import React, { useState } from 'react'; +import { motion } from 'motion/react'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/src/components/ui/card'; +import { Button } from '@/src/components/ui/button'; +import { Input } from '@/src/components/ui/input'; +import { GraduationCap, Calendar, User, Lock, Search, BookOpen, ChevronRight, Award } from 'lucide-react'; +import { cn } from '@/src/lib/utils'; + +export default function School() { + const [activeTab, setActiveTab] = useState<'schedule' | 'grades'>('schedule'); + const [loading, setLoading] = useState(false); + const [queried, setQueried] = useState(false); + + const handleQuery = (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setTimeout(() => { + setLoading(false); + setQueried(true); + }, 1500); + }; + + return ( +
+
+ {/* Query Form */} + + + + + 教务查询 + + 输入教务系统账号密码以同步数据 + + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + +
+ +
+ + +
+
+
+
+ + {/* Data Summary */} + + + + + 数据摘要 + + 当前缓存或最近一次查询结果 + + + {queried ? ( +
+ + + +
+ ) : ( +
+ +

暂无缓存数据,请先执行查询

+
+ )} +
+
+
+ + {/* View Toggle */} +
+ + +
+ + {/* Content Area */} + + {activeTab === 'schedule' ? : } + +
+ ); +} + +function DatabaseIcon(props: any) { + return ( + + + + + + ); +} + +function SummaryItem({ label, value, icon: Icon }: any) { + return ( +
+
+ +
+
+

{label}

+

{value}

+
+
+ ); +} + +function ScheduleView({ queried }: { queried: boolean }) { + if (!queried) { + return ( + + + +

请先查询课表

+
+
+ ); + } + + const days = ['周一', '周二', '周三', '周四', '周五']; + const mockSchedule = [ + { day: 0, time: '08:00 - 09:35', name: '高等数学 (下)', room: '教1-204' }, + { day: 0, time: '10:00 - 11:35', name: '大学物理', room: '教2-101' }, + { day: 1, time: '14:00 - 15:35', name: '软件工程', room: '计科楼 302' }, + { day: 2, time: '08:00 - 09:35', name: '数据结构', room: '教1-105' }, + { day: 3, time: '16:00 - 17:35', name: '计算机网络', room: '计科楼 401' }, + { day: 4, time: '10:00 - 11:35', name: '操作系统', room: '教3-202' }, + ]; + + return ( + + + 本周课表 + + +
+ {days.map((day, index) => ( +
+
+ {day} +
+
+ {mockSchedule.filter(s => s.day === index).map((course, i) => ( +
+

{course.time}

+

{course.name}

+

+ {course.room} +

+
+ ))} + {mockSchedule.filter(s => s.day === index).length === 0 && ( +
+ 无课程 +
+ )} +
+
+ ))} +
+
+
+ ); +} + +function GradesView({ queried }: { queried: boolean }) { + if (!queried) { + return ( + + + +

请先查询成绩

+
+
+ ); + } + + const terms = [ + { + name: '2024 秋', + grades: [75, 78, 80, 83, 85, 88, 89, 96] + }, + { + name: '2025 春', + grades: [70, 78, 82, 84, 85, 85, 86, 88, 93] + }, + { + name: '2025 秋', + grades: [68, 70, 76, 80, 85, 86, 90, 94, 97] + } + ]; + + const getScoreStyle = (score: number) => { + if (score >= 95) return 'bg-[#336EFF]/50 text-white'; + if (score >= 90) return 'bg-[#336EFF]/40 text-white/90'; + if (score >= 85) return 'bg-[#336EFF]/30 text-white/80'; + if (score >= 80) return 'bg-slate-700/60 text-white/70'; + if (score >= 75) return 'bg-slate-700/40 text-white/60'; + return 'bg-slate-800/60 text-white/50'; + }; + + return ( + + + 成绩热力图 + + +
+ {terms.map((term, i) => ( +
+

{term.name}

+
+ {term.grades.map((score, j) => ( +
+ {score} +
+ ))} +
+
+ ))} +
+
+
+ ); +} diff --git a/front/tsconfig.json b/front/tsconfig.json new file mode 100644 index 0000000..d88f175 --- /dev/null +++ b/front/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "allowJs": true, + "jsx": "react-jsx", + "paths": { + "@/*": [ + "./*" + ] + }, + "allowImportingTsExtensions": true, + "noEmit": true + } +} diff --git a/front/vite.config.ts b/front/vite.config.ts new file mode 100644 index 0000000..0506f1b --- /dev/null +++ b/front/vite.config.ts @@ -0,0 +1,24 @@ +import tailwindcss from '@tailwindcss/vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; +import {defineConfig, loadEnv} from 'vite'; + +export default defineConfig(({mode}) => { + const env = loadEnv(mode, '.', ''); + return { + plugins: [react(), tailwindcss()], + define: { + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY), + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + }, + }, + server: { + // HMR is disabled in AI Studio via DISABLE_HMR env var. + // Do not modify—file watching is disabled to prevent flickering during agent edits. + hmr: process.env.DISABLE_HMR !== 'true', + }, + }; +}); diff --git a/scripts/local-smoke.ps1 b/scripts/local-smoke.ps1 new file mode 100644 index 0000000..0576ec6 --- /dev/null +++ b/scripts/local-smoke.ps1 @@ -0,0 +1,130 @@ +$ErrorActionPreference = 'Stop' + +$root = Split-Path -Parent $PSScriptRoot +$backendLogOut = Join-Path $root 'backend-dev.out.log' +$backendLogErr = Join-Path $root 'backend-dev.err.log' +$frontendLogOut = Join-Path $root 'frontend-dev.out.log' +$frontendLogErr = Join-Path $root 'frontend-dev.err.log' +$javaExe = 'C:\Program Files\Java\jdk-22\bin\java.exe' + +Remove-Item $backendLogOut, $backendLogErr, $frontendLogOut, $frontendLogErr -ErrorAction SilentlyContinue + +$backend = Start-Process ` + -FilePath $javaExe ` + -ArgumentList '-jar', 'backend/target/yoyuzh-portal-backend-0.0.1-SNAPSHOT.jar', '--spring.profiles.active=dev' ` + -WorkingDirectory $root ` + -PassThru ` + -RedirectStandardOutput $backendLogOut ` + -RedirectStandardError $backendLogErr + +try { + $backendReady = $false + for ($i = 0; $i -lt 40; $i++) { + Start-Sleep -Seconds 2 + try { + $response = Invoke-WebRequest -Uri 'http://127.0.0.1:8080/swagger-ui.html' -UseBasicParsing -TimeoutSec 3 + if ($response.StatusCode -eq 200) { + $backendReady = $true + break + } + } + catch { + } + } + + if (-not $backendReady) { + throw '后端启动失败' + } + + $userSuffix = Get-Random -Minimum 1000 -Maximum 9999 + $username = "tester$userSuffix" + $email = "tester$userSuffix@example.com" + $password = 'pass123456' + $registerBody = @{ + username = $username + email = $email + password = $password + } | ConvertTo-Json + + $register = Invoke-RestMethod ` + -Uri 'http://127.0.0.1:8080/api/auth/register' ` + -Method Post ` + -ContentType 'application/json' ` + -Body $registerBody + + $token = $register.data.token + if (-not $token) { + throw '注册未返回 token' + } + + $headers = @{ Authorization = "Bearer $token" } + $profile = Invoke-RestMethod -Uri 'http://127.0.0.1:8080/api/user/profile' -Headers $headers -Method Get + if ($profile.data.username -ne $username) { + throw '用户信息校验失败' + } + + Invoke-RestMethod ` + -Uri 'http://127.0.0.1:8080/api/files/mkdir' ` + -Headers $headers ` + -Method Post ` + -ContentType 'application/x-www-form-urlencoded' ` + -Body 'path=/docs' | Out-Null + + $tempFile = Join-Path $root 'backend-upload-smoke.txt' + Set-Content -Path $tempFile -Value 'hello portal' -Encoding UTF8 + & curl.exe -s -X POST -H "Authorization: Bearer $token" -F "path=/docs" -F "file=@$tempFile" http://127.0.0.1:8080/api/files/upload | Out-Null + + $files = Invoke-RestMethod -Uri 'http://127.0.0.1:8080/api/files/list?path=%2Fdocs&page=0&size=10' -Headers $headers -Method Get + if ($files.data.items.Count -lt 1) { + throw '文件列表为空' + } + + $schedule = Invoke-RestMethod -Uri 'http://127.0.0.1:8080/api/cqu/schedule?semester=2025-2026-1&studentId=20230001' -Headers $headers -Method Get + if ($schedule.data.Count -lt 1) { + throw '课表接口为空' + } + + $frontend = Start-Process ` + -FilePath 'cmd.exe' ` + -ArgumentList '/c', 'npm run dev -- --host 127.0.0.1 --port 4173' ` + -WorkingDirectory (Join-Path $root 'vue') ` + -PassThru ` + -RedirectStandardOutput $frontendLogOut ` + -RedirectStandardError $frontendLogErr + + try { + $frontendReady = $false + for ($i = 0; $i -lt 30; $i++) { + Start-Sleep -Seconds 2 + try { + $index = Invoke-WebRequest -Uri 'http://127.0.0.1:4173' -UseBasicParsing -TimeoutSec 3 + if ($index.StatusCode -eq 200) { + $frontendReady = $true + break + } + } + catch { + } + } + + if (-not $frontendReady) { + throw '前端启动失败' + } + + Write-Output "BACKEND_OK username=$username" + Write-Output "FILES_OK count=$($files.data.items.Count)" + Write-Output "SCHEDULE_OK count=$($schedule.data.Count)" + Write-Output 'FRONTEND_OK url=http://127.0.0.1:4173' + } + finally { + if ($frontend -and -not $frontend.HasExited) { + Stop-Process -Id $frontend.Id -Force + } + } +} +finally { + Remove-Item (Join-Path $root 'backend-upload-smoke.txt') -ErrorAction SilentlyContinue + if ($backend -and -not $backend.HasExited) { + Stop-Process -Id $backend.Id -Force + } +} diff --git a/scripts/start-backend-dev.ps1 b/scripts/start-backend-dev.ps1 new file mode 100644 index 0000000..0e42d90 --- /dev/null +++ b/scripts/start-backend-dev.ps1 @@ -0,0 +1,37 @@ +$ErrorActionPreference = 'Stop' + +$root = Split-Path -Parent $PSScriptRoot +$javaExe = 'C:\Program Files\Java\jdk-22\bin\java.exe' +$out = Join-Path $root 'backend-dev.out.log' +$err = Join-Path $root 'backend-dev.err.log' + +if (Test-Path $out) { + Remove-Item $out -Force +} +if (Test-Path $err) { + Remove-Item $err -Force +} + +$proc = Start-Process ` + -FilePath $javaExe ` + -ArgumentList '-jar', 'backend/target/yoyuzh-portal-backend-0.0.1-SNAPSHOT.jar', '--spring.profiles.active=dev' ` + -WorkingDirectory $root ` + -PassThru ` + -RedirectStandardOutput $out ` + -RedirectStandardError $err + +Start-Sleep -Seconds 10 + +try { + $resp = Invoke-WebRequest -Uri 'http://127.0.0.1:8080/swagger-ui.html' -UseBasicParsing -TimeoutSec 5 + Write-Output "PID=$($proc.Id)" + Write-Output "STATUS=$($resp.StatusCode)" + Write-Output 'URL=http://127.0.0.1:8080/swagger-ui.html' +} +catch { + Write-Output "PID=$($proc.Id)" + Write-Output 'STATUS=STARTED_BUT_NOT_READY' + if (Test-Path $err) { + Get-Content -Tail 40 $err + } +} diff --git a/scripts/start-frontend-dev.ps1 b/scripts/start-frontend-dev.ps1 new file mode 100644 index 0000000..f3ccaec --- /dev/null +++ b/scripts/start-frontend-dev.ps1 @@ -0,0 +1,36 @@ +$ErrorActionPreference = 'Stop' + +$root = Split-Path -Parent $PSScriptRoot +$frontendLogOut = Join-Path $root 'frontend-dev.out.log' +$frontendLogErr = Join-Path $root 'frontend-dev.err.log' + +if (Test-Path $frontendLogOut) { + Remove-Item $frontendLogOut -Force +} +if (Test-Path $frontendLogErr) { + Remove-Item $frontendLogErr -Force +} + +$proc = Start-Process ` + -FilePath 'cmd.exe' ` + -ArgumentList '/c', 'npm run dev -- --host 127.0.0.1 --port 4173' ` + -WorkingDirectory (Join-Path $root 'vue') ` + -PassThru ` + -RedirectStandardOutput $frontendLogOut ` + -RedirectStandardError $frontendLogErr + +Start-Sleep -Seconds 6 + +try { + $resp = Invoke-WebRequest -Uri 'http://127.0.0.1:4173' -UseBasicParsing -TimeoutSec 5 + Write-Output "PID=$($proc.Id)" + Write-Output "STATUS=$($resp.StatusCode)" + Write-Output 'URL=http://127.0.0.1:4173' +} +catch { + Write-Output "PID=$($proc.Id)" + Write-Output 'STATUS=STARTED_BUT_NOT_READY' + if (Test-Path $frontendLogErr) { + Get-Content -Tail 40 $frontendLogErr + } +} diff --git a/todo_list.md b/todo_list.md deleted file mode 100644 index c56a7eb..0000000 --- a/todo_list.md +++ /dev/null @@ -1,267 +0,0 @@ -下面这份是**工程级**的 TODO List(Markdown),按“能上线”的路径拆好了:里程碑 → 任务 → 验收点。你前端已经做了一部分,就从 **FE-Desktop / FE-Apps** 里把已完成的勾上即可。 - ---- - -# Web Desktop 项目工程 TODO(可上线版) - -> 维护规则: -> -> * 每个任务尽量做到“可交付 + 可验收”。 -> * 任务粒度:1~4 小时能完成为宜。 -> * 每周至少推进一个 Milestone 到可演示状态。 - ---- - -## 0. 里程碑总览 - -* [ ] **M0:工程骨架就绪(能跑通 dev / staging)** -* [ ] **M1:账号体系 + 桌面壳可用(基础可演示)** -* [ ] **M2:网盘 MVP(OSS 直传闭环)** -* [ ] **M3:分享/审计/配额/管理后台(上线门槛)** -* [ ] **M4:Campus BFF 接 Rust API(课表/成绩缓存降级)** -* [ ] **M5:论坛/地图完善 + 监控告警 + 上线演练** - ---- - -## 1. M0 工程骨架就绪 - -### Repo / 工程结构 - -* [ ] 初始化 mono-repo 或多 repo 结构(建议:`frontend/` `backend/` `infra/`) -* [ ] 统一 lint/format(ESLint/Prettier + 后端 formatter) -* [ ] 统一 commit 规范(可选:commitlint) -* [ ] 统一环境变量模板:`.env.example`(前后端分开) -* [ ] 基础 README:本地启动、部署、配置项说明 - -### 本地开发环境 - -* [ ] docker-compose:db + redis + backend + (可选) nginx -* [ ] 一键启动脚本:`make dev` / `npm run dev:all` -* [ ] staging 配置:独立域名/反代/证书(哪怕自签) - -### 基础 CI(至少跑检查) - -* [ ] PR 触发:lint + typecheck + unit test(最小集合) -* [ ] build 产物:frontend build / backend build - -**验收点** - -* [ ] 新电脑 clone 后 30 分钟内能跑起来(含 db) - ---- - -## 2. M1 账号体系 + 桌面壳 - -### BE-Auth - -* [ ] 用户注册/登录(JWT + refresh 或 session 二选一) -* [ ] 密码加密(argon2/bcrypt) -* [ ] `GET /auth/me` -* [ ] 登录失败限流(例如 5 次/5 分钟) -* [ ] 基础用户状态:normal / banned -* [ ] request_id 全链路(middleware) - -### FE-Auth - -* [ ] 登录/注册/找回页面 -* [ ] token/会话续期策略 -* [ ] 全局错误处理(统一 toast + request_id) - -### FE-Desktop(你已做一部分:这里把你已有的勾上) - -* [ ] 桌面布局:图标/分组/壁纸/主题 -* [ ] 窗口系统:打开/关闭/最小化/最大化/拖拽/层级 -* [ ] 最近使用 / 收藏 -* [ ] 全局搜索:应用搜索(先做) -* [ ] 通知中心壳(先只做 UI) - -### BE-Desktop - -* [ ] user_settings 表:layout/theme/wallpaper -* [ ] `GET /desktop/settings` / `PUT /desktop/settings` -* [ ] `GET /desktop/apps`(服务端下发应用配置,方便后续开关) - -**验收点** - -* [ ] 新用户登录后能看到桌面;布局修改刷新后不丢 -* [ ] 被封禁用户无法登录(提示明确) - ---- - -## 3. M2 网盘 MVP(OSS 直传闭环) - -### BE-Drive 元数据 - -* [ ] files 表(user_id, parent_id, name, size, mime, oss_key, deleted_at…) -* [ ] 目录增删改查:create folder / rename / move / list -* [ ] 软删除 + 回收站 list/restore -* [ ] 文件名净化(防 XSS/路径注入) - -### BE-OSS 直传 - -* [ ] `POST /drive/upload/init`:生成 oss_key + STS/Policy(带过期时间) -* [ ] 分片策略:chunk_size / multipart(建议直接支持) -* [ ] `POST /drive/upload/complete`:写入元数据(校验 size/etag) -* [ ] `GET /drive/download/{id}`:签名 URL(短期有效) -* [ ] 下载审计:记录 download_sign - -### FE-Drive - -* [ ] 文件列表:分页/排序/面包屑 -* [ ] 上传:小文件 + 大文件分片 + 断点续传 -* [ ] 上传队列:暂停/继续/失败重试 -* [ ] 预览:图片/PDF/文本 -* [ ] 删除/恢复/彻底删除(回收站) -* [ ] 文件搜索(文件名) - -**验收点** - -* [ ] 上传→列表出现→预览/下载→删除→回收站恢复闭环 -* [ ] 网络断开后能续传(至少同一次会话内) - ---- - -## 4. M3 分享 / 审计 / 配额 / 管理后台(上线门槛) - -### BE-Share - -* [ ] 创建分享:有效期、提取码、权限(预览/下载) -* [ ] 分享访问页:`GET /share/{token}` -* [ ] 下载:`POST /share/{token}/download`(校验提取码后返回签名 URL) -* [ ] 撤销分享:立即失效 -* [ ] 分享访问审计(ip/ua/time/count) - -### BE-Quota & RateLimit - -* [ ] 用户配额:总容量、单文件大小、日上传/日下载 -* [ ] 配额校验:upload/init、complete、download/sign -* [ ] 限流:登录、绑定校园、成绩刷新、签名下载、分享访问 - -### BE-Audit - -* [ ] audit_logs:关键操作埋点(upload_init/upload_complete/download_sign/share_create…) -* [ ] 查询接口:按 user/action/time 过滤(管理员) - -### Admin(最小管理后台) - -* [ ] 用户管理:封禁/解封 -* [ ] 配额配置:默认值 + 单用户覆盖(可选) -* [ ] OSS 配置:bucket/STS 策略(至少可查看) -* [ ] 审计查询页 - -**验收点** - -* [ ] 超配额时前后端提示一致且不可绕过 -* [ ] 分享链接可用、可撤销、访问可审计 -* [ ] 管理员能查到关键操作日志 - ---- - -## 5. M4 Campus BFF(接 Rust API:课表/成绩) - -> 核心:**平台后端不让前端直连 Rust API**,统一做鉴权、缓存、熔断、错误码映射。 - -### BE-Campus 绑定与凭据 - -* [ ] `POST /campus/bind`:绑定校园账号(加密存储 credential / 或保存 rust session_token) -* [ ] `POST /campus/unbind`:解绑并删除凭据 -* [ ] 凭据加密:密钥不入库(env + KMS 可选) -* [ ] 绑定/查询限流(防封控) - -### BE-Campus Rust API 网关层 - -* [ ] Rust API client:超时、重试(只读)、熔断 -* [ ] 健康检查:/healthz 探测 + 指标 -* [ ] DTO 适配层:Rust 返回字段变化不直接打爆前端 -* [ ] 错误码映射:Rust error → 平台 error code - -### BE-Campus 缓存与降级 - -* [ ] campus_cache:课表/成绩 TTL(课表 12h,成绩 24h) -* [ ] 手动刷新冷却时间(成绩建议更长) -* [ ] Rust 不可用时返回缓存 + 标注更新时间 - -### FE-Campus - -* [ ] 绑定页面(学号/密码或 token) -* [ ] 课表周视图/日视图 -* [ ] 成绩学期视图 + 列表 -* [ ] “刷新”按钮(带冷却提示) -* [ ] “数据更新时间 / 当前为缓存”提示 - -**验收点** - -* [ ] Rust API 挂了:仍能展示缓存且不白屏 -* [ ] 频繁刷新会被限流并提示 - ---- - -## 6. M5 论坛/地图完善 + 监控告警 + 上线演练 - -### Forum(按 Rust API 能力) - -* [ ] 板块列表/帖子列表/详情/评论 -* [ ] 发帖/评论(幂等键 Idempotency-Key) -* [ ] 内容风控:频率限制 + 基础敏感词(最小) -* [ ] 举报入口(最小) -* [ ] 通知:回复/提及(站内通知) - -### Map - -* [ ] POI 展示:分类 + 搜索 -* [ ] 地图 SDK 接入(Leaflet/高德/腾讯择一) -* [ ] POI 缓存 7d + 更新策略 -* [ ](可选)POI 后台维护 - -### Observability(上线前必须补) - -* [ ] 指标:API 错误率、P95、Rust 成功率、OSS 上传失败率 -* [ ] 日志:结构化 + request_id -* [ ] 告警:Rust 健康异常、错误率激增、DB/Redis 异常 -* [ ] 错误追踪:Sentry 或同类(可选但强建议) - -### 安全加固(上线前必做清单) - -* [ ] CSP/安全头(X-Frame-Options 等) -* [ ] 上传文件类型限制 + 文件名净化 -* [ ] 权限回归测试:越权访问用例全覆盖 -* [ ] Secrets 全部迁移到安全配置(不进仓库) - -### 上线演练 - -* [ ] staging 环境全链路演练(含 OSS、Rust API) -* [ ] 灰度发布流程(最小:可回滚) -* [ ] 数据库备份与恢复演练 -* [ ] 压测(最少测下载签名/列表/校园查询) - -**验收点** - -* [ ] staging → prod 一键发布可回滚 -* [ ] 关键告警触发能收到(邮件/IM 随便一种) - ---- - -## 7. 你当前“前端已做一部分”的对齐清单(快速标记) - -把你已经完成的模块在这里勾上,方便我后续给你拆“下一步最优先做什么”: - -* [ ] 桌面图标布局 -* [ ] 窗口拖拽/层级 -* [ ] 应用打开/关闭/最小化 -* [ ] 主题/壁纸 -* [ ] 网盘 UI(列表/上传面板/预览) -* [ ] 校园 UI(课表/成绩/论坛/地图) -* [ ] 游戏应用容器 - ---- - -## 8. 最小上线 Checklist(不做这些别上线) - -* [ ] 后端鉴权与资源隔离(不可只靠前端) -* [ ] OSS 长期密钥不下发前端(只给 STS/签名) -* [ ] 下载签名短期有效 + 审计 -* [ ] 限流(登录/绑定/校园刷新/签名下载/分享访问) -* [ ] Rust API 超时/熔断/缓存降级 -* [ ] 结构化日志 + request_id -* [ ] staging 环境演练 + 回滚方案 - diff --git a/vue/.gitignore b/vue/.gitignore deleted file mode 100644 index a547bf3..0000000 --- a/vue/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/vue/.vscode/extensions.json b/vue/.vscode/extensions.json deleted file mode 100644 index a7cea0b..0000000 --- a/vue/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["Vue.volar"] -} diff --git a/vue/README.md b/vue/README.md deleted file mode 100644 index 4e00c2c..0000000 --- a/vue/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Vue 3 + TypeScript + Vite - -This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` - - - - - - - - - - - - - - - - - diff --git a/vue/public/race/input.js b/vue/public/race/input.js deleted file mode 100644 index 690618a..0000000 --- a/vue/public/race/input.js +++ /dev/null @@ -1,402 +0,0 @@ -'use strict'; - -const gamepadsEnable = enhancedMode; -const inputWASDEmulateDirection = enhancedMode; -const allowTouch = enhancedMode; -const isTouchDevice = allowTouch && window.ontouchstart !== undefined; -const touchGamepadEnable = enhancedMode; -const touchGamepadAlpha = .3; - -/////////////////////////////////////////////////////////////////////////////// -// Input user functions - -const keyIsDown = (key) => inputData[key] & 1; -const keyWasPressed = (key) => inputData[key] & 2 ? 1 : 0; -const keyWasReleased = (key) => inputData[key] & 4 ? 1 : 0; -const clearInput = () => inputData = []; - -let mousePos = vec3(); -const mouseIsDown = keyIsDown; -const mouseWasPressed = keyWasPressed; -const mouseWasReleased = keyWasReleased; - -let isUsingGamepad; -const gamepadIsDown = (key, gamepad=0) => !!(gamepadData[gamepad][key] & 1); -const gamepadWasPressed = (key, gamepad=0) => !!(gamepadData[gamepad][key] & 2); -const gamepadWasReleased = (key, gamepad=0) => !!(gamepadData[gamepad][key] & 4); -const gamepadStick = (stick, gamepad=0) => - gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec3() : vec3(); -const gamepadGetValue = (key, gamepad=0) => gamepadDataValues[gamepad][key]; - -/////////////////////////////////////////////////////////////////////////////// -// Input event handlers - -let inputData = []; // track what keys are down - -function inputInit() -{ - if (gamepadsEnable) - { - gamepadData = []; - gamepadStickData = []; - gamepadDataValues = []; - gamepadData[0] = []; - gamepadDataValues[0] = []; - } - - onkeydown = (e)=> - { - isUsingGamepad = 0; - if (!e.repeat) - { - inputData[e.code] = 3; - if (inputWASDEmulateDirection) - inputData[remapKey(e.code)] = 3; - } - } - - onkeyup = (e)=> - { - inputData[e.code] = 4; - if (inputWASDEmulateDirection) - inputData[remapKey(e.code)] = 4; - } - - // mouse event handlers - onmousedown = (e)=> - { - isUsingGamepad = 0; - inputData[e.button] = 3; - mousePos = mouseToScreen(vec3(e.x,e.y)); - } - onmouseup = (e)=> inputData[e.button] = inputData[e.button] & 2 | 4; - onmousemove = (e)=> - { - mousePos = mouseToScreen(vec3(e.x,e.y)); - if (freeCamMode) - { - mouseDelta.x += e.movementX/mainCanvasSize.x; - mouseDelta.y += e.movementY/mainCanvasSize.y; - } - } - oncontextmenu = (e)=> false; // prevent right click menu - - // handle remapping wasd keys to directions - const remapKey = (c) => inputWASDEmulateDirection ? - c == 'KeyW' ? 'ArrowUp' : - c == 'KeyS' ? 'ArrowDown' : - c == 'KeyA' ? 'ArrowLeft' : - c == 'KeyD' ? 'ArrowRight' : c : c; - - // init touch input - isTouchDevice && touchInputInit(); -} - -function inputUpdate() -{ - // clear input when lost focus (prevent stuck keys) - isTouchDevice || document.hasFocus() || clearInput(); - gamepadsEnable && gamepadsUpdate(); -} - -function inputUpdatePost() -{ - // clear input to prepare for next frame - for (const i in inputData) - inputData[i] &= 1; -} - -// convert a mouse position to screen space -const mouseToScreen = (mousePos) => -{ - if (!clampAspectRatios) - { - // canvas always takes up full screen - return vec3(mousePos.x/mainCanvasSize.x,mousePos.y/mainCanvasSize.y); - } - else - { - const rect = mainCanvas.getBoundingClientRect(); - return vec3(percent(mousePos.x, rect.left, rect.right), percent(mousePos.y, rect.top, rect.bottom)); - } -} - -/////////////////////////////////////////////////////////////////////////////// -// gamepad input - -// gamepad internal variables -let gamepadData, gamepadStickData, gamepadDataValues; - -// gamepads are updated by engine every frame automatically -function gamepadsUpdate() -{ - const applyDeadZones = (v)=> - { - const min=.2, max=.8; - const deadZone = (v)=> - v > min ? percent( v, min, max) : - v < -min ? -percent(-v, min, max) : 0; - return vec3(deadZone(v.x), deadZone(-v.y)).clampLength(); - } - - // update touch gamepad if enabled - isTouchDevice && touchGamepadUpdate(); - - // return if gamepads are disabled or not supported - if (!navigator || !navigator.getGamepads) - return; - - // only poll gamepads when focused or in debug mode (allow playing when not focused in debug) - if (!devMode && !document.hasFocus()) - return; - - // poll gamepads - const gamepads = navigator.getGamepads(); - for (let i = gamepads.length; i--;) - { - // get or create gamepad data - const gamepad = gamepads[i]; - const data = gamepadData[i] || (gamepadData[i] = []); - const dataValue = gamepadDataValues[i] || (gamepadDataValues[i] = []); - const sticks = gamepadStickData[i] || (gamepadStickData[i] = []); - - if (gamepad) - { - // read analog sticks - for (let j = 0; j < gamepad.axes.length-1; j+=2) - sticks[j>>1] = applyDeadZones(vec3(gamepad.axes[j],gamepad.axes[j+1])); - - // read buttons - for (let j = gamepad.buttons.length; j--;) - { - const button = gamepad.buttons[j]; - const wasDown = gamepadIsDown(j,i); - data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0; - dataValue[j] = percent(button.value||0,.1,.9); // apply deadzone - isUsingGamepad ||= !i && button.pressed; - } - - const gamepadDirectionEmulateStick = 1; - if (gamepadDirectionEmulateStick) - { - // copy dpad to left analog stick when pressed - const dpad = vec3( - (gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1), - (gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1)); - if (dpad.lengthSquared()) - sticks[0] = dpad.clampLength(); - } - } - } -} - -/////////////////////////////////////////////////////////////////////////////// -// touch input - -// try to enable touch mouse -function touchInputInit() -{ - // add non passive touch event listeners - let handleTouch = handleTouchDefault; - if (touchGamepadEnable) - { - // touch input internal variables - handleTouch = handleTouchGamepad; - touchGamepadButtons = []; - touchGamepadStick = vec3(); - } - document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false }); - document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false }); - document.addEventListener('touchend', (e) => handleTouch(e), { passive: false }); - - // override mouse events - onmousedown = onmouseup = ()=> 0; - - // handle all touch events the same way - let wasTouching; - function handleTouchDefault(e) - { - // fix stalled audio requiring user interaction - if (soundEnable && !audioContext) - audioContext = new AudioContext; // create audio context - //if (soundEnable && audioContext && audioContext.state != 'running') - // sound_bump.play(); // play sound to fix audio - - // check if touching and pass to mouse events - const touching = e.touches.length; - const button = 0; // all touches are left mouse button - if (touching) - { - // average all touch positions - const p = vec3(); - for (let touch of e.touches) - { - p.x += touch.clientX/e.touches.length; - p.y += touch.clientY/e.touches.length; - } - - mousePos = mouseToScreen(p); - wasTouching ? 0 : inputData[button] = 3; - } - else if (wasTouching) - inputData[button] = inputData[button] & 2 | 4; - - // set was touching - wasTouching = touching; - - // prevent default handling like copy and magnifier lens - if (document.hasFocus()) // allow document to get focus - e.preventDefault(); - - // must return true so the document will get focus - return true; - } -} - -/////////////////////////////////////////////////////////////////////////////// -// touch gamepad - -// touch gamepad internal variables -let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick, touchGamepadSize; - -// special handling for virtual gamepad mode -function handleTouchGamepad(e) -{ - if (soundEnable) - { - if (!audioContext) - audioContext = new AudioContext; // create audio context - - // fix stalled audio - if (audioContext.state != 'running') - audioContext.resume(); - } - - // clear touch gamepad input - touchGamepadStick = vec3(); - touchGamepadButtons = []; - isUsingGamepad = true; - - const touching = e.touches.length; - if (touching) - { - touchGamepadTimer.set(); - if (paused || titleScreenMode || gameOverTimer.isSet()) - { - // touch anywhere to press start - touchGamepadButtons[9] = 1; - return; - } - } - - // get center of left and right sides - const stickCenter = vec3(touchGamepadSize, mainCanvasSize.y-touchGamepadSize); - const buttonCenter = mainCanvasSize.subtract(vec3(touchGamepadSize, touchGamepadSize)); - const startCenter = mainCanvasSize.scale(.5); - - // check each touch point - for (const touch of e.touches) - { - let touchPos = mouseToScreen(vec3(touch.clientX, touch.clientY)); - touchPos = touchPos.multiply(mainCanvasSize); - if (touchPos.distance(stickCenter) < touchGamepadSize) - { - // virtual analog stick - touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize); - //touchGamepadStick = touchGamepadStick.clampLength(); // circular clamp - touchGamepadStick.x = clamp(touchGamepadStick.x,-1,1); - touchGamepadStick.y = clamp(touchGamepadStick.y,-1,1); - } - else if (touchPos.distance(buttonCenter) < touchGamepadSize) - { - // virtual face buttons - const button = touchPos.y > buttonCenter.y ? 1 : 0; - touchGamepadButtons[button] = 1; - } - else if (touchPos.distance(startCenter) < touchGamepadSize) - { - // hidden virtual start button in center - touchGamepadButtons[9] = 1; - } - } - - // call default touch handler so normal touch events still work - //handleTouchDefault(e); - - // prevent default handling like copy and magnifier lens - if (document.hasFocus()) // allow document to get focus - e.preventDefault(); - - // must return true so the document will get focus - return true; -} - -// update the touch gamepad, called automatically by the engine -function touchGamepadUpdate() -{ - if (!touchGamepadEnable) - return; - - // adjust for thin canvas - touchGamepadSize = clamp(mainCanvasSize.y/8, 99, mainCanvasSize.x/2); - - ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!'); - if (!touchGamepadTimer.isSet()) - return; - - // read virtual analog stick - const sticks = gamepadStickData[0] || (gamepadStickData[0] = []); - sticks[0] = touchGamepadStick.copy(); - - // read virtual gamepad buttons - const data = gamepadData[0]; - for (let i=10; i--;) - { - const wasDown = gamepadIsDown(i,0); - data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0; - } -} - -// render the touch gamepad, called automatically by the engine -function touchGamepadRender() -{ - if (!touchGamepadEnable || !touchGamepadTimer.isSet()) - return; - - // fade off when not touching or paused - const alpha = percent(touchGamepadTimer.get(), 4, 3); - if (!alpha || paused) - return; - - // setup the canvas - const context = mainContext; - context.save(); - context.globalAlpha = alpha*touchGamepadAlpha; - context.strokeStyle = '#fff'; - context.lineWidth = 3; - - // draw left analog stick - context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000'; - context.beginPath(); - - // draw circle shaped gamepad - const leftCenter = vec3(touchGamepadSize, mainCanvasSize.y-touchGamepadSize); - context.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9); - context.fill(); - context.stroke(); - - // draw right face buttons - const rightCenter = vec3(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize); - for (let i=2; i--;) - { - const pos = rightCenter.add(vec3(0,(i?1:-1)*touchGamepadSize/2)); - context.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000'; - context.beginPath(); - context.arc(pos.x, pos.y, touchGamepadSize/3, 0, 9); - context.fill(); - context.stroke(); - } - - // set canvas back to normal - context.restore(); -} \ No newline at end of file diff --git a/vue/public/race/levels.js b/vue/public/race/levels.js deleted file mode 100644 index 9bca181..0000000 --- a/vue/public/race/levels.js +++ /dev/null @@ -1,447 +0,0 @@ -'use strict'; - -let levelInfoList; - -function initLevelInfos() -{ - levelInfoList = []; - let LI, level=0; - - // Level 1 - beach - - LI = new LevelInfo(level++, [ - spriteList.grass_plain, - spriteList.tree_palm, - spriteList.rock_big, - ], spriteList.tree_palm); - LI.horizonSpriteSize = .7; - LI.waterSide = -1; - //LI.tunnel = spriteList.tunnel2; // test tunnel - LI.billboardChance = .3 // more billboards at start - //LI.trafficDensity = .7; // less traffic start - - // mostly straight with few well defined turns or bumps - LI.turnChance = .6; - LI.turnMin = .2; - //LI.turnMax = .6; - //LI.bumpChance = .5; - LI.bumpFreqMin = .2; - LI.bumpFreqMax = .4; - LI.bumpScaleMin = 10; - LI.bumpScaleMax = 20; - - // Level 2 - forest - - LI = new LevelInfo(level++, [ - spriteList.tree_oak, - spriteList.grass_plain, - spriteList.tree_bush, - spriteList.tree_stump, - spriteList.grass_flower1, - spriteList.grass_flower3, - spriteList.grass_flower2, - ], spriteList.tree_bush, spriteList.horizon_smallMountains); - LI.horizonSpriteSize = 10; - LI.trackSideRate = 10; - LI.sceneryListBias = 9; - //LI.skyColorTop = WHITE; - LI.skyColorBottom = hsl(.5,.3,.5); - LI.roadColor = hsl(.05,.4,.2); - LI.groundColor = hsl(.2,.4,.4); - LI.cloudColor = hsl(0,0,1,.3); - LI.cloudHeight = .2; - LI.sunHeight = .7; - LI.billboardChance = .1 // less billboards in forest type areas - //LI.trafficDensity = .7; // less traffic in forest - - // trail through forest - LI.turnChance = .7; // more small turns - //LI.turnMin = 0; - //LI.turnMax = .6; - LI.bumpChance = .8; - LI.bumpFreqMin = .4; - //LI.bumpFreqMax = .7; - //LI.bumpScaleMin = 50; - LI.bumpScaleMax = 140; - - // Level 3 - desert - - // has long straight thin roads and tunnel - LI = new LevelInfo(level++, [ - spriteList.grass_dead, - spriteList.tree_dead, - spriteList.rock_big, - spriteList.tree_stump, - ], spriteList.telephonePole, spriteList.horizon_desert); - LI.trackSideRate = 50; - LI.trackSideChance = 1; - LI.skyColorTop = hsl(.15,1,.9); - LI.skyColorBottom = hsl(.5,.7,.6); - LI.roadColor = hsl(.1,.2,.2); - LI.lineColor = hsl(0,0,1,.5); - LI.groundColor = hsl(.1,.2,.5); - LI.trackSideForce = 1; // telephone poles on right side - LI.cloudHeight = .05; - LI.sunHeight = .9; - LI.sideStreets = 1; - LI.laneCount = 2; - LI.hazardType = spriteList.hazard_sand; - LI.hazardChance = .005; - LI.tunnel = spriteList.tunnel2; - LI.trafficDensity = .7; // less traffic in desert, only 2 lanes - LI.billboardRate = 87; - LI.billboardScale = 8; - - // flat desert - //LI.turnChance = .5; - LI.turnMin = .2; - LI.turnMax = .6; - LI.bumpChance = 1; - //LI.bumpFreqMin = 0; - LI.bumpFreqMax = .2; - LI.bumpScaleMin = 30; - LI.bumpScaleMax = 60; - - // Level 4 - snow area - - LI = new LevelInfo(level++, [ - spriteList.grass_snow, - spriteList.tree_dead, - spriteList.tree_snow, - spriteList.rock_big, - spriteList.tree_stump, - ], spriteList.tree_snow, spriteList.horizon_snow); - LI.sceneryListBias = 9; - LI.trackSideRate = 21; - LI.skyColorTop = hsl(.5,.2,.4); - LI.skyColorBottom = WHITE; - LI.roadColor = hsl(0,0,.5,.5); - LI.groundColor = hsl(.6,.3,.9); - LI.cloudColor = hsl(0,0,.8,.5); - LI.horizonSpriteSize = 2; - LI.lineColor = hsl(0,0,1,.5); - LI.sunHeight = .7; - LI.hazardType = spriteList.hazard_rocks; - LI.hazardChance = .002; - LI.trafficDensity = 1.2; // extra traffic through snow - - // snowy mountains - //LI.turnChance = .5; - LI.turnMin = .4; - //LI.turnMax = .6; - LI.bumpChance = .8; - LI.bumpFreqMin = .2; - LI.bumpFreqMax = .6; - //LI.bumpFreqMax = .7; - LI.bumpScaleMin = 50; - LI.bumpScaleMax = 100; - - // Level 5 - canyon - - // has winding roads, hills, and sand onground - LI = new LevelInfo(level++, [ - spriteList.rock_huge, - spriteList.grass_dead, - spriteList.tree_fall, - spriteList.rock_huge2, - spriteList.grass_flower2, - spriteList.tree_dead, - spriteList.tree_stump, - spriteList.rock_big, - ], spriteList.tree_fall,spriteList.horizon_brownMountains); - LI.sceneryListBias = 2; - LI.trackSideRate = 31; - LI.skyColorTop = hsl(.7,1,.7); - LI.skyColorBottom = hsl(.2,1,.9); - LI.roadColor = hsl(0,0,.15); - LI.groundColor = hsl(.1,.4,.5); - LI.cloudColor = hsl(0,0,1,.3); - LI.cloudHeight = .1; - LI.sunColor = hsl(0,1,.7); - //LI.laneCount = 3; - LI.billboardChance = .1 // less billboards in forest type areas - LI.trafficDensity = .7; // less traffic in canyon - - // rocky canyon - LI.turnChance = 1; // must turn to block vision - LI.turnMin = .2; - LI.turnMax = .8; - LI.bumpChance = .9; - LI.bumpFreqMin = .4; - //LI.bumpFreqMax = .7; - //LI.bumpScaleMin = 50; - LI.bumpScaleMax = 120; - - // Level 6 - red fields and city - LI = new LevelInfo(level++, [ - spriteList.grass_red, - spriteList.tree_yellow, - spriteList.rock_big, - spriteList.tree_stump, - //spriteList.rock_wide, - ], spriteList.tree_yellow,spriteList.horizon_city); - LI.trackSideRate = 31; - LI.skyColorTop = YELLOW; - LI.skyColorBottom = RED; - LI.roadColor = hsl(0,0,.1); - LI.lineColor = hsl(.15,1,.7); - LI.groundColor = hsl(.05,.5,.4); - LI.cloudColor = hsl(.15,1,.5,.5); - //LI.cloudHeight = .3; - LI.billboardRate = 23; // more billboards in city - LI.billboardChance = .5 - LI.horizonSpriteSize = 1.5; - if (!js13kBuildLevel2) - LI.horizonFlipChance = .3; - LI.sunHeight = .5; - LI.sunColor = hsl(.15,1,.8); - LI.sideStreets = 1; - LI.laneCount = 5; - LI.trafficDensity = 2; // extra traffic in city - - // in front of city - LI.turnChance = .3; - LI.turnMin = .5 - LI.turnMax = .9; // bigger turns since lanes are wide - //LI.bumpChance = .5; - LI.bumpFreqMin = .3; - LI.bumpFreqMax = .6; - LI.bumpScaleMin = 80; - LI.bumpScaleMax = 200; - - // Level 7 - graveyard - - LI = new LevelInfo(level++, [ - spriteList.grass_dead, - spriteList.grass_plain, - spriteList.grave_stone, - spriteList.tree_dead, - spriteList.tree_stump, - ], spriteList.tree_oak, spriteList.horizon_graveyard); - LI.sceneryListBias = 2; - LI.trackSideRate = 50; - LI.skyColorTop = hsl(.5,1,.5); - LI.skyColorBottom = hsl(0,1,.8); - LI.roadColor = hsl(.6,.3,.15); - LI.groundColor = hsl(.2,.3,.5); - LI.lineColor = hsl(0,0,1,.5); - LI.billboardChance = 0; // no ads in graveyard - LI.cloudColor = hsl(.15,1,.9,.3); - LI.horizonSpriteSize = 4; - LI.sunHeight = 1.5; - //LI.laneCount = 3; - //LI.trafficDensity = .7; - LI.trackSideChance = 1; // more trees - - // thin road over hills in graveyard - //LI.turnChance = .5; - LI.turnMax = .6; - LI.bumpChance = .6; - LI.bumpFreqMin = LI.bumpFreqMax = .7; - LI.bumpScaleMin = 80; - //LI.bumpScaleMax = 150; - - // Level 8 - jungle - dirt road, many trees - // has lots of physical hazards - LI = new LevelInfo(level++, [ - spriteList.grass_large, - spriteList.tree_palm, - spriteList.grass_flower1, - spriteList.rock_tall, - spriteList.rock_big, - spriteList.rock_huge2, - ], spriteList.rock_big, spriteList.horizon_redMountains); - LI.sceneryListBias = 5; - LI.trackSideRate = 25; - LI.skyColorTop = hsl(0,1,.8); - LI.skyColorBottom = hsl(.6,1,.6); - LI.lineColor = hsl(0,0,0,0); - LI.roadColor = hsl(0,.6,.2,.8); - LI.groundColor = hsl(.1,.5,.4); - LI.waterSide = 1; - LI.cloudColor = hsl(0,1,.96,.8); - LI.cloudWidth = .6; - //LI.cloudHeight = .3; - LI.sunHeight = .7; - LI.sunColor = hsl(.1,1,.7); - LI.hazardType = spriteList.rock_big; - LI.hazardChance = .2; - LI.trafficDensity = 0; // no other cars in jungle - - // bumpy jungle road - LI.turnChance = .8; - //LI.turnMin = 0; - LI.turnMax = .3; // lots of slight turns - LI.bumpChance = 1; - LI.bumpFreqMin = .4; - LI.bumpFreqMax = .6; - LI.bumpScaleMin = 10; - LI.bumpScaleMax = 80; - - // Level 9 - strange area - LI = new LevelInfo(level++, [ - spriteList.grass_red, - spriteList.rock_weird, - spriteList.tree_huge, - ], spriteList.rock_weird2, spriteList.horizon_weird); - LI.trackSideRate = 50; - LI.skyColorTop = hsl(.05,1,.8); - LI.skyColorBottom = hsl(.15,1,.7); - LI.lineColor = hsl(0,1,.9); - LI.roadColor = hsl(.6,1,.1); - LI.groundColor = hsl(.6,1,.6); - LI.cloudColor = hsl(.9,1,.5,.3); - LI.cloudHeight = .2; - LI.sunColor = BLACK; - LI.laneCount = 4; - LI.trafficDensity = 1.5; // extra traffic to increase difficulty here - - // large strange hills - LI.turnChance = .7; - LI.turnMin = .3; - LI.turnMax = .8; - LI.bumpChance = 1; - LI.bumpFreqMin = .5; - LI.bumpFreqMax = .9; - LI.bumpScaleMin = 100; - LI.bumpScaleMax = 200; - - // Level 10 - mountains - hilly, rocks on sides - LI = new LevelInfo(level++, [ - spriteList.grass_plain, - spriteList.rock_huge3, - spriteList.grass_flower1, - spriteList.rock_huge2, - spriteList.rock_huge, - ], spriteList.tree_pink); - LI.trackSideRate = 21; - LI.skyColorTop = hsl(.2,1,.9); - LI.skyColorBottom = hsl(.55,1,.5); - LI.roadColor = hsl(0,0,.1); - LI.groundColor = hsl(.1,.5,.7); - LI.cloudColor = hsl(0,0,1,.5); - LI.tunnel = spriteList.tunnel1; - if (js13kBuildLevel2) - LI.horizonSpriteSize = 0; - else - { - LI.sunHeight = .6; - LI.horizonSprite = spriteList.horizon_mountains - LI.horizonSpriteSize = 1; - } - - // mountains, most difficult level - LI.turnChance = LI.turnMax = .8; - //LI.turnMin = 0; - LI.bumpChance = 1; - LI.bumpFreqMin = .3; - LI.bumpFreqMax = .9; - //LI.bumpScaleMin = 50; - LI.bumpScaleMax = 80; - - // Level 11 - win area - LI = new LevelInfo(level++, [ - spriteList.grass_flower1, - spriteList.grass_flower2, - spriteList.grass_flower3, - spriteList.grass_plain, - spriteList.tree_oak, - spriteList.tree_bush, - ], spriteList.tree_oak); - LI.sceneryListBias = 1; - LI.groundColor = hsl(.2,.3,.5); - LI.trackSideRate = LI.billboardChance = 0; - LI.bumpScaleMin = 1e3; // hill in the distance - - // match settings to previous level - if (js13kBuildLevel2) - LI.horizonSpriteSize = 0; - else - { - LI.sunHeight = .6; - LI.horizonSprite = spriteList.horizon_mountains - LI.horizonSpriteSize = 1; - } -} - -const getLevelInfo = (level) => testLevelInfo || levelInfoList[level|0] || levelInfoList[0]; - -// info about how to build and draw each level -class LevelInfo -{ - constructor(level, scenery, trackSideSprite,horizonSprite=spriteList.horizon_islands) - { - // add self to list - levelInfoList[level] = this; - - if (debug) - { - for(const s of scenery) - ASSERT(s, 'missing scenery!'); - } - - this.level = level; - this.scenery = scenery; - this.trackSideSprite = trackSideSprite; - this.sceneryListBias = 29; - this.waterSide = 0; - - this.billboardChance = .2; - this.billboardRate = 45; - this.billboardScale = 1; - this.trackSideRate = 5; - this.trackSideForce = 0; - this.trackSideChance = .5; - - this.groundColor = hsl(.08,.2, .7); - this.skyColorTop = WHITE; - this.skyColorBottom = hsl(.57,1,.5); - this.lineColor = WHITE; - this.roadColor = hsl(0, 0, .5); - - // horizon stuff - this.cloudColor = hsl(.15,1,.95,.7); - this.cloudWidth = 1; - this.cloudHeight = .3; - this.horizonSprite = horizonSprite; - this.horizonSpriteSize = 2; - this.sunHeight = .8; - this.sunColor = hsl(.15,1,.95); - - // track generation - this.laneCount = 3; - this.trafficDensity = 1; - - // default turns and bumps - this.turnChance = .5; - this.turnMin = 0; - this.turnMax = .6; - this.bumpChance = .5; - this.bumpFreqMin = 0; // no bumps - this.bumpFreqMax = .7; // more often bumps - this.bumpScaleMin = 50; // rapid bumps - this.bumpScaleMax = 150; // largest hills - } - - randomize() - { - shuffle(this.scenery); - this.sceneryListBias = random.float(5,30); - this.groundColor = random.mutateColor(this.groundColor); - this.skyColorTop = random.mutateColor(this.skyColorTop); - this.skyColorBottom = random.mutateColor(this.skyColorBottom); - this.lineColor = random.mutateColor(this.lineColor); - this.roadColor = random.mutateColor(this.roadColor); - this.cloudColor = random.mutateColor(this.cloudColor); - this.sunColor = random.mutateColor(this.sunColor); - - // track generation - this.laneCount = random.int(2,5); - this.trafficDensity = random.float(.5,1.5); - - // default turns and bumps - this.turnChance = random.float(); - this.turnMin = random.float(); - this.turnMax = random.float(); - this.bumpChance = random.float(); - this.bumpFreqMin = random.float(.5); // no bumps - this.bumpFreqMax = random.float(); // more often bumps - this.bumpScaleMin = random.float(20,50); // rapid bumps - this.bumpScaleMax = random.float(50,150); // largest hills - this.hazardChance = 0; - } -} \ No newline at end of file diff --git a/vue/public/race/main.js b/vue/public/race/main.js deleted file mode 100644 index ecb3e05..0000000 --- a/vue/public/race/main.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -/* - -Dr1v3n Wild by Frank Force -A 13k game for js13kGames 2024 - -Controls -- Arrows or Mouse = Drive -- Spacebar = Brake -- F = Free Ride Mode -- Escape = Title Screen - -Features -- 10 stages with unique visuals -- Fast custom WebGL rendering -- Procedural art (trees, rocks, scenery) -- Track generator -- Arcade style driving physics -- 2 types of AI vehicles -- Parallax horizon and sky -- ZZFX sounds -- Persistent save data -- Keyboard or mouse input -- All written from scratch in vanilla JS - -*/ - -/////////////////////////////////////////////////// - -// debug settings -//devMode = debugInfo = 1 -//soundVolume = 0 -//debugGenerativeCanvas = 1 -//autoPause = 0 -//quickStart = 1 -//disableAiVehicles = 1 - -/////////////////////////////////////////////////// - -gameInit(); \ No newline at end of file diff --git a/vue/public/race/release.js b/vue/public/race/release.js deleted file mode 100644 index dc81a7e..0000000 --- a/vue/public/race/release.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -const debug = 0; -const enhancedMode = 1; -let debugInfo, debugMesh, debugTile, debugGenerativeCanvas, devMode; -const js13kBuildLevel2 = 0; // more space is needed for js13k - -// disable debug features -function ASSERT() {} -function debugInit() {} -function drawDebug() {} -function debugUpdate() {} -function debugSaveCanvas() {} -function debugSaveText() {} -function debugDraw() {} -function debugSaveDataURL() {} \ No newline at end of file diff --git a/vue/public/race/releaseJS13K.js b/vue/public/race/releaseJS13K.js deleted file mode 100644 index b216599..0000000 --- a/vue/public/race/releaseJS13K.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -const debug = 0; -let debugInfo, debugMesh, debugTile, debugGenerativeCanvas, devMode, enhancedMode; -const js13kBuildLevel2 = 1; // more space is needed for js13k - -// disable debug features -function ASSERT() {} -function debugInit() {} -function drawDebug() {} -function debugUpdate() {} -function debugSaveCanvas() {} -function debugSaveText() {} -function debugDraw() {} -function debugSaveDataURL() {} \ No newline at end of file diff --git a/vue/public/race/scene.js b/vue/public/race/scene.js deleted file mode 100644 index 18d0760..0000000 --- a/vue/public/race/scene.js +++ /dev/null @@ -1,120 +0,0 @@ -'use strict'; - -function drawScene() -{ - drawSky(); - drawTrack(); - drawCars(); - drawTrackScenery(); -} - -function drawSky() -{ - glEnableLighting = glEnableFog = 0; - glSetDepthTest(0,0); - random.setSeed(13); - - // lerp level stuff - const levelFloat = cameraOffset/checkpointDistance; - const levelInfo = getLevelInfo(levelFloat); - const levelInfoLast = getLevelInfo(levelFloat-1); - const levelPercent = levelFloat%1; - const levelLerpPercent = percent(levelPercent, 0, levelLerpRange); - - // sky - const skyTop = 13e2; // slightly above camera - const skyZ = 1e3; - const skyW = 5e3; - const skyH = 8e2; - { - // top/bottom gradient - const skyColorTop = levelInfoLast.skyColorTop.lerp(levelInfo.skyColorTop, levelLerpPercent); - const skyColorBottom = levelInfoLast.skyColorBottom.lerp(levelInfo.skyColorBottom, levelLerpPercent); - pushGradient(vec3(0,skyH,skyZ).addSelf(cameraPos), vec3(skyW,skyH), skyColorTop, skyColorBottom); - - // light settings from sky - glLightDirection = vec3(0,1,1).rotateY(worldHeading).normalize(); - glLightColor = skyColorTop.lerp(WHITE,.8).lerp(BLACK,.1); - glAmbientColor = skyColorBottom.lerp(WHITE,.8).lerp(BLACK,.6); - glFogColor = skyColorBottom.lerp(WHITE,.5); - } - - const headingScale = -5e3; - const circleSpriteTile = spriteList.circle.spriteTile; - const dotSpriteTile = spriteList.dot.spriteTile; - { - // sun - const sunSize = 2e2; - const sunHeight = skyTop*lerp(levelLerpPercent, levelInfoLast.sunHeight, levelInfo.sunHeight); - const sunColor = levelInfoLast.sunColor.lerp(levelInfo.sunColor, levelLerpPercent); - const x = mod(worldHeading+PI,2*PI)-PI; - for(let i=0;i<1;i+=.05) - { - sunColor.a = i?(1-i)**7:1; - pushSprite(vec3( x*headingScale, sunHeight, skyZ).addSelf(cameraPos), vec3(sunSize*(1+i*30)), sunColor, i?dotSpriteTile:circleSpriteTile); - } - } - - // clouds - const range = 1e4; - const windSpeed = 50; - for(let i=99;i--;) - { - const cloudColor = levelInfoLast.cloudColor.lerp(levelInfo.cloudColor, levelLerpPercent); - const cloudWidth = lerp(levelLerpPercent, levelInfoLast.cloudWidth, levelInfo.cloudWidth); - const cloudHeight = lerp(levelLerpPercent, levelInfoLast.cloudHeight, levelInfo.cloudHeight); - - let x = worldHeading*headingScale + random.float(range) + time*windSpeed*random.float(1,1.5); - x = mod(x,range) - range/2; - const y = random.float(skyTop); - const s = random.float(3e2,8e2); - pushSprite(vec3( x, y, skyZ).addSelf(cameraPos), vec3(s*cloudWidth,s*cloudHeight), cloudColor, dotSpriteTile) - } - - // parallax - const horizonSprite = levelInfo.horizonSprite; - const horizonSpriteTile = horizonSprite.spriteTile; - const horizonSpriteSize = levelInfo.horizonSpriteSize; - for(let i=99;i--;) - { - const p = i/99; - const ltp = lerp(p,1,.5); - const ltt = .1; - const levelTransition = levelFloat<.5 || levelFloat > levelGoal-.5 ? 1 : levelPercent < ltt ? (levelPercent/ltt)**ltp : - levelPercent > 1-ltt ? 1-((levelPercent-1)/ltt+1)**ltp : 1; - - const parallax = lerp(p, .9, .98); - const s = random.float(1e2,2e2)*horizonSpriteSize* lerp(p,1,.5) - const size = vec3(random.float(1,2)*(horizonSprite.canMirror ? s*random.sign() : s),s,s); - const x = mod(worldHeading*headingScale/parallax + random.float(range),range) - range/2; - - const yMax = size.y*.75; - if (!js13kBuildLevel2 && levelInfo.horizonFlipChance) - { - // horizon spites that can be flipped vertically - if (random.bool(levelInfo.horizonFlipChance)) - size.y *= -1; - } - const y = lerp(levelTransition, -yMax*1.5, yMax); - const c = horizonSprite.getRandomSpriteColor(); - pushSprite(vec3( x, y, skyZ).addSelf(cameraPos), size, c, horizonSpriteTile); - } - - { - // get ahead of player for horizon ground color to match track - const lookAhead = .2; - const levelFloatAhead = levelFloat + lookAhead; - const levelInfo = getLevelInfo(levelFloatAhead); - const levelInfoLast = getLevelInfo(levelFloatAhead-1); - const levelPercent = levelFloatAhead%1; - const levelLerpPercent = percent(levelPercent, 0, levelLerpRange); - - // horizon bottom - const groundColor = levelInfoLast.groundColor.lerp(levelInfo.groundColor, levelLerpPercent).brighten(.1); - pushSprite(vec3(0,-skyH,skyZ).addSelf(cameraPos), vec3(skyW,skyH), groundColor); - } - - glRender(); - glSetDepthTest(); - glEnableLighting = glEnableFog = 1; -} \ No newline at end of file diff --git a/vue/public/race/sounds.js b/vue/public/race/sounds.js deleted file mode 100644 index 61d700f..0000000 --- a/vue/public/race/sounds.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const sound_beep = new Sound([,0,220,.01,.08,.05,,.5,,,,,,,,,.3,.9,.01,,-99]); // beep -const sound_engine = new Sound([,,40,.2,.5,.5,,,,,,,,300,,,,,,,-80]); // engine -const sound_hit = new Sound([,.3,90,,,.2,,3,,,,,,9,,.3,,.3,.01]); // crash -const sound_bump = new Sound([4,.2,400,.01,.01,.01,,.8,-60,-70,,,.03,.1,,,.1,.5,.01,.4,400]); // bump -const sound_checkpoint = new Sound([.3,0,980,,,,,3,,,,,,,,.03,,,,,500]); // checkpoint -const sound_win = new Sound([1.5,,110,.04,,2,,6,,1,330,.07,.05,,,,.4,.8,,.5,1e3]); // win -const sound_lose = new Sound([,,120,.1,,1,,3,,.6,,,,1,,.2,.4,.1,1,,500]); // lose \ No newline at end of file diff --git a/vue/public/race/track.js b/vue/public/race/track.js deleted file mode 100644 index 817cff9..0000000 --- a/vue/public/race/track.js +++ /dev/null @@ -1,425 +0,0 @@ -'use strict'; - -function trackPreUpdate() -{ - // calcuate track x offsets and projections (iterate in reverse) - const cameraTrackInfo = new TrackSegmentInfo(cameraOffset); - const cameraTrackSegment = cameraTrackInfo.segmentIndex; - const cameraTrackSegmentPercent = cameraTrackInfo.percent; - const turnScale = 2; - for(let x=0, v=0, i=0; i pos.z*4+2e3) - return; // out of view - if (pos.z < 0) - return; // behind camera - } - - const shadowScale = sprite.shadowScale; - const wind = sprite.windScale * trackWind; - const yShadowOffset = freeCamMode ? cameraPos.y/20 : 10; // fix shadows in free cam mode - const spriteYOffset = scale.y*(1+sprite.spriteYOffset) + (freeCamMode?cameraPos.y/20:0); - - pos.y += yShadowOffset; - if (shadowScale) - pushShadow(pos, scale.y*shadowScale, scale.y*shadowScale/6); - - // draw on top of shadow - pos.y += spriteYOffset - yShadowOffset; - pushSprite(pos, scale, color, sprite.spriteTile, wind); -} - -/////////////////////////////////////////////////////////////////////////////// - -/*function draw3DTrackScenery() -{ - const cameraTrackSegment = cameraTrackInfo.segmentIndex; - - // 3d scenery - for(let i=drawDistance, segment1, segment2; i--; ) - { - segment2 = segment1; - const segmentIndex = cameraTrackSegment+i; - segment1 = track[segmentIndex]; - if (!segment1 || !segment2) - continue; - - if (segmentIndex%7) - continue - - const d = segment1.pos.subtract(segment2.pos); - const heading = PI-Math.atan2(d.x, d.z); - - // random scenery - random.setSeed(trackSeed+segmentIndex); - const w = segment1.width; - const o =(segmentIndex%2?1:-1)*(random.float(5e4,1e5)) - const r = vec3(0,-heading,0); - const p = vec3(-o,0).addSelf(segment1.pos); - - const s = vec3(random.float(500,1e3),random.float(1e3,4e3),random.float(500,1e3)); - //const s = vec3(500,random.float(2e3,2e4),500); - const m4 = buildMatrix(p,r,s); - const c = hsl(0,0,random.float(.2,1)); - cubeMesh.render(m4, c); - } -} -*/ - -/////////////////////////////////////////////////////////////////////////////// - -// an instance of a sprite -class TrackObject -{ - constructor(trackSegment, sprite, offset, color=WHITE, sizeScale=1) - { - this.trackSegment = trackSegment; - this.sprite = sprite; - this.offset = offset; - this.color = color; - - const scale = sprite.size * sizeScale; - this.scale = vec3(scale); - const trackWidth = trackSegment.width; - const trackside = offset.x < trackWidth*2 && offset.x > -trackWidth*2; - if (trackside && sprite.trackFace) - this.scale.x *= sign(offset.x); - else if (sprite.canMirror && random.bool()) - this.scale.x *= -1; - this.collideSize = sprite.collideScale*abs(scale); - } - - draw() - { - const trackSegment = this.trackSegment; - const pos = trackSegment.pos.add(this.offset); - const wind = trackSegment.getWind(); - pushTrackObject(pos, this.scale, this.color, this.sprite, wind); - } -} - -class TrackSegment -{ - constructor(segmentIndex,offset,width) - { - if (segmentIndex >= levelGoal*checkpointTrackSegments) - width = 0; // no track after end - - this.offset = offset; - this.width = width; - this.pitch = 0; - this.normal = vec3(); - - this.trackObjects = []; - const levelFloat = segmentIndex/checkpointTrackSegments; - const level = this.level = testLevelInfo ? testLevelInfo.level : levelFloat|0; - const levelInfo = getLevelInfo(level); - const levelInfoNext = getLevelInfo(levelFloat+1); - const levelLerpPercent = percent(levelFloat%1, 1-levelLerpRange, 1); - - const checkpointLine = segmentIndex > 25 && segmentIndex < 30 - || segmentIndex%checkpointTrackSegments > checkpointTrackSegments-10; - const recordPoint = bestDistance/trackSegmentLength; - const recordPointLine = segmentIndex>>3 == recordPoint>>3; - this.sideStreet = levelInfo.sideStreets && ((segmentIndex%checkpointTrackSegments)%495<36); - - { - // setup colors - const groundColor = levelInfo.groundColor.lerp(levelInfoNext.groundColor,levelLerpPercent); - const lineColor = levelInfo.lineColor.lerp(levelInfoNext.lineColor,levelLerpPercent); - const roadColor = levelInfo.roadColor.lerp(levelInfoNext.roadColor,levelLerpPercent); - - const largeSegmentIndex = segmentIndex/9|0; - const stripe = largeSegmentIndex% 2 ? .1: 0; - this.colorGround = groundColor.brighten(Math.cos(segmentIndex*2/PI)/20); - this.colorRoad = roadColor.brighten(stripe&&.05); - if (recordPointLine) - this.colorRoad = hsl(0,.8,.5); - else if (checkpointLine) - this.colorRoad = WHITE; // starting line - this.colorLine = lineColor; - if (stripe) - this.colorLine.a = 0; - if (this.sideStreet) - this.colorLine = this.colorGround = this.colorRoad; - } - - // spawn track objects - if (debug && testGameSprite) - { - // test sprite - this.addSprite(testGameSprite,random.floatSign(width/2,1e4)); - } - else if (debug && testTrackBillboards) - { - // test billboard - const billboardSprite = random.fromList(spriteList.billboards); - this.addSprite(billboardSprite,random.floatSign(width/2,1e4)); - } - else if (segmentIndex == levelGoal*checkpointTrackSegments) - { - // goal! - this.addSprite(spriteList.sign_goal); - } - else if (segmentIndex%checkpointTrackSegments == 0) - { - // checkpoint - if (segmentIndex < levelGoal*checkpointTrackSegments) - { - this.addSprite(spriteList.sign_checkpoint1,-width+500); - this.addSprite(spriteList.sign_checkpoint2, width-500); - } - } - - if (segmentIndex == 30) - { - // starting area - this.addSprite(spriteList.sign_start); - - // left - const ol = -(width+100); - this.addSprite(spriteList.sign_opGames,ol,1450); - this.addSprite(spriteList.sign_zzfx,ol,850); - this.addSprite(spriteList.sign_avalanche,ol); - - // right - const or = width+100; - this.addSprite(spriteList.sign_frankForce,or,1500); - this.addSprite(spriteList.sign_github,or,350); - this.addSprite(spriteList.sign_js13k,or); - if (js13kBuild) - random.seed = 1055752394; // hack, reset seed for js13k - } - } - - getWind() - { - const offset = this.offset; - const noiseScale = .001; - return Math.sin(time+(offset.x+offset.z)*noiseScale)/2; - } - - addSprite(sprite,x=0,y=0,extraScale=1) - { - // add a sprite to the track as a new track object - const offset = vec3(x,y); - const sizeScale = extraScale*sprite.getRandomSpriteScale(); - const color = sprite.getRandomSpriteColor(); - const trackObject = new TrackObject(this, sprite, offset, color, sizeScale); - this.trackObjects.push(trackObject); - } -} - -// get lerped info about a track segment -class TrackSegmentInfo -{ - constructor(z) - { - const segment = this.segmentIndex = z/trackSegmentLength|0; - const percent = this.percent = z/trackSegmentLength%1; - if (track[segment] && track[segment+1]) - { - if (track[segment].pos && track[segment+1].pos) - this.pos = track[segment].pos.lerp(track[segment+1].pos, percent); - else - this.pos = vec3(0,0,z); - this.pitch = lerp(percent, track[segment].pitch, track[segment+1].pitch); - this.offset = track[segment].offset.lerp(track[segment+1].offset, percent); - this.width = lerp(percent, track[segment].width,track[segment+1].width); - } - else - this.offset = this.pos = vec3(this.pitch = this.width = 0,0,z); - } -} diff --git a/vue/public/race/trackGen.js b/vue/public/race/trackGen.js deleted file mode 100644 index 49e833f..0000000 --- a/vue/public/race/trackGen.js +++ /dev/null @@ -1,274 +0,0 @@ -'use strict'; - -const testTrackBillboards=0; - -// build the road with procedural generation -function buildTrack() -{ - // set random seed & time - random.setSeed(trackSeed); - track = []; - - let sectionXEndDistance = 0; - let sectionYEndDistance = 0; - let sectionTurn = 0; - let noisePos = random.int(1e5); - let sectionBumpFrequency = 0; - let sectionBumpScale = 1; - let currentNoiseFrequency = 0; - let currentNoiseScale = 1; - - let turn = 0; - - // generate the road - const trackEnd = levelGoal*checkpointTrackSegments; - const roadTransitionRange = testQuick?min(checkpointTrackSegments,500):500; - for(let i=0; i < trackEnd + 5e4; ++i) - { - const levelFloat = i/checkpointTrackSegments; - const level = levelFloat|0; - const levelInfo = getLevelInfo(level); - const levelInfoLast = getLevelInfo(levelFloat-1); - const levelLerpPercent = percent(i%checkpointTrackSegments, 0, roadTransitionRange); - - if (js13kBuild && i==31496) - random.setSeed(7); // mess with seed to randomize jungle - - const roadGenWidth = laneWidth/2*lerp(levelLerpPercent, levelInfoLast.laneCount, levelInfo.laneCount); - - let height = 0; - let width = roadGenWidth; - - const startOfTrack = !level && i < 400; - const checkpointSegment = i%checkpointTrackSegments; - const levelBetweenRange = 100; - let isBetweenLevels = checkpointSegment < levelBetweenRange || - checkpointSegment > checkpointTrackSegments - levelBetweenRange; - isBetweenLevels |= startOfTrack; // start of track - //const nextCheckpoint = (level+1)*checkpointTrackSegments; - - if (isBetweenLevels) - { - // transition at start or end of level - sectionXEndDistance = sectionYEndDistance = sectionTurn = 0; - } - else - { - // turns - const turnChance = levelInfo.turnChance; // chance of turn - const turnMin = levelInfo.turnMin; // min turn - const turnMax = levelInfo.turnMax; // max turn - const sectionDistanceMin = 100; - const sectionDistanceMax = 400; - if (sectionXEndDistance-- < 0) - { - // pick random section distance - sectionXEndDistance = random.int(sectionDistanceMin,sectionDistanceMax); - sectionTurn = random.bool(turnChance) ? random.floatSign(turnMin,turnMax) : 0; - } - - // bumps - const bumpChance = levelInfo.bumpChance; // chance of bump - const bumpFreqMin = levelInfo.bumpFreqMin; // no bumps - const bumpFreqMax = levelInfo.bumpFreqMax; // raipd bumps - const bumpScaleMin = levelInfo.bumpScaleMin; // small rapid bumps - const bumpScaleMax = levelInfo.bumpScaleMax; // large hills - if (sectionYEndDistance-- < 0) - { - // pick random section distance - sectionYEndDistance = random.int(sectionDistanceMin,sectionDistanceMax); - if (random.bool(bumpChance)) - { - sectionBumpFrequency = random.float(bumpFreqMin,bumpFreqMax); - sectionBumpScale = random.float(bumpScaleMin,bumpScaleMax); - } - else - { - sectionBumpFrequency = 0; - sectionBumpScale = bumpScaleMin; - } - } - } - - if (i > trackEnd - 500) - sectionTurn = 0; // no turns at end - - turn = lerp(.02,turn, sectionTurn); // smooth out turns - - // apply noise to height - const noiseFrequency = currentNoiseFrequency - = lerp(.01, currentNoiseFrequency, sectionBumpFrequency); - const noiseSize = currentNoiseScale - = lerp(.01, currentNoiseScale, sectionBumpScale); - - //noiseFrequency = 1; noiseSize = 50; - if (currentNoiseFrequency) - noisePos += noiseFrequency/noiseSize; - const noiseConstant = 20; - height = noise1D(noisePos)*noiseConstant*noiseSize; - - //turn = .7; height = 0; - //turn = Math.sin(i/100)*.7; - //height = noise1D((i-50)/99)*2700;turn =0; // jumps test - - // create track segment - const o = vec3(turn, height, i*trackSegmentLength); - track[i] = new TrackSegment(i, o, width); - } - - // second pass - let hazardWait = 0; - let tunnelOn = 0; - let tunnelTime = 0; - let trackSideChanceScale = 1; - for(let i=0; i < track.length; ++i) - { - // calculate pitch - const iCheckpoint = i%checkpointTrackSegments; - const t = track[i]; - const levelInfo = getLevelInfo(t.level); - ASSERT(t.level == levelInfo.level || t.level > levelGoal); - - const previous = track[i-1]; - if (previous) - { - t.pitch = Math.atan2(previous.offset.y-t.offset.y, trackSegmentLength); - const d = vec3(0,t.offset.y-previous.offset.y, trackSegmentLength); - t.normal = d.cross(vec3(1,0)).normalize(); - } - - if (!iCheckpoint) - { - // reset level settings - trackSideChanceScale = 1; - } - - if (t.sideStreet || i < 50) - { - tunnelOn = 0; - continue; // no objects on side streets - } - - // check what kinds of turns are ahead - const lookAheadTurn = 150; - const lookAheadStep = 20; - let leftTurns = 0, rightTurns = 0; - for(let k=0; k 0) leftTurns = max(leftTurns, x); - else rightTurns = max(rightTurns, -x); - } - } - - // spawn road signs - const roadSignRate = 10; - const turnWarning = 0.5; - let signSide; - if (i < levelGoal*checkpointTrackSegments) // end of level - if (rightTurns > turnWarning || leftTurns > turnWarning) - { - // turn - signSide = sign(rightTurns - leftTurns); - if (i%roadSignRate == 0) - t.addSprite(spriteList.sign_turn,signSide*(t.width+500)); - } - - // todo prevent sprites from spawning near road signs? - //levelInfo.tunnel = spriteList.tunnel2; // test tuns - if (levelInfo.tunnel) - { - const isRockArch = levelInfo.tunnel.tunnelArch; - const isLongTunnel = levelInfo.tunnel.tunnelLong; - if (iCheckpoint > 100 && iCheckpoint < checkpointTrackSegments - 100) - { - const wasOn = tunnelOn; - if (tunnelTime-- < 0) - { - tunnelOn = !tunnelOn; - tunnelTime = tunnelOn? - isRockArch ? 10 : random.int(200,600) : - tunnelTime = random.int(300,600); // longer when off - } - - if (tunnelOn) - { - // brighter front of tunnel - const sprite = isLongTunnel && !wasOn ? - spriteList.tunnel2Front : levelInfo.tunnel; - t.addSprite(sprite, 0); - - if (isLongTunnel && i%50==0) - { - // lights on top of tunnel - const lightSprite = spriteList.light_tunnel; - const tunnelHeight = 1600; - t.addSprite(lightSprite, 0, tunnelHeight); - } - continue; - } - } - } - else - { - // restart tunnel wait - tunnelOn = tunnelTime = 0; - } - - { - // sprites on sides of track - const billboardChance = levelInfo.billboardChance; - const billboardRate = levelInfo.billboardRate; - if (i%billboardRate == 0 && random.bool(billboardChance)) - { - // random billboards - const extraScale = levelInfo.billboardScale; // larger in desert - const width = t.width*extraScale; - const count = spriteList.billboards.length; - const billboardSprite = spriteList.billboards[random.int(count)]; - const billboardSide = signSide ? -signSide : random.sign(); - t.addSprite(billboardSprite,billboardSide*random.float(width+600,width+800),0,extraScale); - continue; - } - if (levelInfo.trackSideSprite) - { - // vary how often side objects spawn - if (random.bool(.001)) - { - trackSideChanceScale = - random.bool(.4) ? 1 : // normal to spawn often - random.bool(.1) ? 0 : // small chance of none - random.float(); // random scale - } - - // track side objects - const trackSideRate = levelInfo.trackSideRate; - const trackSideChance = levelInfo.trackSideChance; - if (i%trackSideRate == 0 && random.bool(trackSideChance*trackSideChanceScale)) - { - const trackSideForce = levelInfo.trackSideForce; - const side = trackSideForce || (i%(trackSideRate*2) 40 && iCheckpoint < checkpointTrackSegments - 40) - if (hazardWait-- < 0 && levelInfo.hazardType && random.bool(levelInfo.hazardChance)) - { - // hazards on the ground in road to slow player - const sprite = levelInfo.hazardType; - t.addSprite(sprite,random.floatSign(t.width/.9)); - - // wait to spawn another hazard - hazardWait = random.float(40,80); - } - } - } -} \ No newline at end of file diff --git a/vue/public/race/utilities.js b/vue/public/race/utilities.js deleted file mode 100644 index 91f5c9f..0000000 --- a/vue/public/race/utilities.js +++ /dev/null @@ -1,229 +0,0 @@ -'use strict'; - -/////////////////////////////////////////////////////////////////////////////// -// Math Stuff - -const PI = Math.PI; -const abs = (value) => Math.abs(value); -const min = (valueA, valueB) => Math.min(valueA, valueB); -const max = (valueA, valueB) => Math.max(valueA, valueB); -const sign = (value) => value < 0 ? -1 : 1; -const mod = (dividend, divisor=1) => ((dividend % divisor) + divisor) % divisor; -const clamp = (value, min=0, max=1) => value < min ? min : value > max ? max : value; -const clampAngle = (value) => ((value+PI) % (2*PI) + 2*PI) % (2*PI) - PI; -const percent = (value, valueA, valueB) => (valueB-=valueA) ? clamp((value-valueA)/valueB) : 0; -const lerp = (percent, valueA, valueB) => valueA + clamp(percent) * (valueB-valueA); -const rand = (valueA=1, valueB=0) => lerp(Math.random(), valueA, valueB); -const randInt = (valueA, valueB=0) => rand(valueA, valueB)|0; -const smoothStep = (p) => p * p * (3 - 2 * p); -const isOverlapping = (posA, sizeA, posB, sizeB=vec3()) => - abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x && abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y; -function buildMatrix(pos, rot, scale) -{ - const R2D = 180/PI; - let m = new DOMMatrix; - pos && m.translateSelf(pos.x, pos.y, pos.z); - rot && m.rotateSelf(rot.x*R2D, rot.y*R2D, rot.z*R2D); - scale && m.scaleSelf(scale.x, scale.y, scale.z); - return m; -} -function shuffle(array) -{ - for(let currentIndex = array.length; currentIndex;) - { - const randomIndex = random.int(currentIndex--); - [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; - } - return array; -} -function formatTimeString(t) -{ - const timeS = t%60|0; - const timeM = t/60|0; - const timeMS = t%1*1e3|0; - return `${timeM}:${timeS<10?'0'+timeS:timeS}.${(timeMS<10?'00':timeMS<100?'0':'')+timeMS}`; -} - -function noise1D(x) -{ - const hash = x=>(new Random(x)).float(-1,1); - return lerp(smoothStep(mod(x,1)), hash(x), hash(x+1)); -} - -/////////////////////////////////////////////////////////////////////////////// -// Vector3 - -const vec3 = (x, y, z)=> y == undefined && z == undefined ? new Vector3(x, x, x) : new Vector3(x, y, z); -const isVector3 = (v) => v instanceof Vector3; -const isNumber = (value) => typeof value === 'number'; -const ASSERT_VEC3 = (v) => ASSERT(isVector3(v)); - -class Vector3 -{ - constructor(x=0, y=0, z=0) - { - ASSERT(isNumber(x) && isNumber(y) && isNumber(z)); - this.x=x; this.y=y; this.z=z; - } - copy() { return vec3(this.x, this.y, this.z); } - add(v) { ASSERT_VEC3(v); return vec3(this.x + v.x, this.y + v.y, this.z + v.z); } - addSelf(v) { ASSERT_VEC3(v); this.x += v.x, this.y += v.y, this.z += v.z; return this } - subtract(v) { ASSERT_VEC3(v); return vec3(this.x - v.x, this.y - v.y, this.z - v.z); } - multiply(v) { ASSERT_VEC3(v); return vec3(this.x * v.x, this.y * v.y, this.z * v.z); } - divide(v) { ASSERT_VEC3(v); return vec3(this.x / v.x, this.y / v.y, this.z / v.z); } - scale(s) { ASSERT(isNumber(s)); return vec3(this.x * s, this.y * s, this.z * s); } - length() { return this.lengthSquared()**.5; } - lengthSquared() { return this.x**2 + this.y**2 + this.z**2; } - distance(v) { ASSERT_VEC3(v); return this.distanceSquared(v)**.5; } - distanceSquared(v) { ASSERT_VEC3(v); return this.subtract(v).lengthSquared(); } - normalize(length=1) { const l = this.length(); return l ? this.scale(length/l) : vec3(length); } - clampLength(length=1) { const l = this.length(); return l > length ? this.scale(length/l) : this; } - dot(v) { ASSERT_VEC3(v); return this.x*v.x + this.y*v.y + this.z*v.z; } - angleBetween(v) { ASSERT_VEC3(v); return Math.acos(clamp(this.dot(v), -1, 1)); } - clamp(a, b) { return vec3(clamp(this.x, a, b), clamp(this.y, a, b), clamp(this.z, a, b)); } - cross(v) { ASSERT_VEC3(v); return vec3(this.y*v.z-this.z*v.y, this.z*v.x-this.x*v.z, this.x*v.y-this.y*v.x); } - lerp(v, p) { ASSERT_VEC3(v); return v.subtract(this).scale(clamp(p)).addSelf(this); } - rotateX(a) - { - const c=Math.cos(a), s=Math.sin(a); - return vec3(this.x, this.y*c - this.z*s, this.y*s + this.z*c); - } - rotateY(a) - { - const c=Math.cos(a), s=Math.sin(a); - return vec3(this.x*c - this.z*s, this.y, this.x*s + this.z*c); - } - rotateZ(a) - { - const c=Math.cos(a), s=Math.sin(a); - return vec3(this.x*c - this.y*s, this.x*s + this.y*c, this.z); - } - transform(matrix) - { - const p = matrix.transformPoint(this); - return vec3(p.x, p.y, p.z); - } - getHSLColor(a=1) { return hsl(this.x, this.y, this.z, a); } -} - -/////////////////////////////////////////////////////////////////////////////// -// Color - -const rgb = (r, g, b, a) => new Color(r, g, b, a); -const hsl = (h, s, l, a) => rgb().setHSLA(h, s, l, a); -const isColor = (c) => c instanceof Color; - -class Color -{ - constructor(r=1, g=1, b=1, a=1) - { - this.r = r; - this.g = g; - this.b = b; - this.a = a; - } - - copy() { return rgb(this.r, this.g, this.b, this.a); } - - lerp(c, percent) - { - ASSERT(isColor(c)); - percent = clamp(percent); - return rgb( - lerp(percent, this.r, c.r), - lerp(percent, this.g, c.g), - lerp(percent, this.b, c.b), - lerp(percent, this.a, c.a), - ); - } - - brighten(amount=.1) - { - return rgb - ( - clamp(this.r + amount), - clamp(this.g + amount), - clamp(this.b + amount), - this.a - ); - } - - setHSLA(h=0, s=0, l=1, a=1) - { - h = mod(h,1); - s = clamp(s); - l = clamp(l); - const q = l < .5 ? l*(1+s) : l+s-l*s, p = 2*l-q, - f = (p, q, t)=> - (t = mod(t,1))*6 < 1 ? p+(q-p)*6*t : - t*2 < 1 ? q : - t*3 < 2 ? p+(q-p)*(4-t*6) : p; - this.r = f(p, q, h + 1/3); - this.g = f(p, q, h); - this.b = f(p, q, h - 1/3); - this.a = a; - return this; - } - - toString() - { return `rgb(${this.r*255},${this.g*255},${this.b*255},${this.a})`; } -} - -/////////////////////////////////////////////////////////////////////////////// -// Random - -class Random -{ - constructor(seed) { this.setSeed(seed); } - setSeed(seed) - { - this.seed = seed+1|0; - this.float();this.float();this.float();// warmup - } - float(a=1, b=0) - { - // xorshift - this.seed ^= this.seed << 13; - this.seed ^= this.seed >>> 17; - this.seed ^= this.seed << 5; - if (js13kBuild) - return b + (a-b) * Math.abs(this.seed % 1e9) / 1e9; // bias low values due to float error - else - return b + (a-b) * Math.abs(this.seed % 1e8) / 1e8; - } - floatSign(a, b) { return this.float(a,b) * this.sign(); } - int(a, b) { return this.float(a, b)|0; } - bool(chance = .5) { return this.float() < chance; } - sign() { return this.bool() ? -1 : 1; } - circle(radius=0, bias = .5) - { - const r = this.float()**bias*radius; - const a = this.float(PI*2); - return vec3(r*Math.cos(a), r*Math.sin(a)); - } - mutateColor(color, amount=.1, brightnessAmount=0) - { - return rgb - ( - clamp(random.float(1,1-brightnessAmount)*(color.r + this.floatSign(amount))), - clamp(random.float(1,1-brightnessAmount)*(color.g + this.floatSign(amount))), - clamp(random.float(1,1-brightnessAmount)*(color.b + this.floatSign(amount))), - color.a - ); - } - fromList(list,startBias=1) { return list[this.float()**startBias*list.length|0]; } -} - -/////////////////////////////////////////////////////////////////////////////// - -class Timer -{ - constructor(timeLeft) - { this.time = timeLeft == undefined ? undefined : time + timeLeft; } - set(timeLeft=0) { this.time = time + timeLeft; } - unset() { this.time = undefined; } - isSet() { return this.time != undefined; } - active() { return time < this.time; } - elapsed() { return time >= this.time; } - get() { return this.isSet()? time - this.time : 0; } -} \ No newline at end of file diff --git a/vue/public/race/vehicle.js b/vue/public/race/vehicle.js deleted file mode 100644 index 8d15de8..0000000 --- a/vue/public/race/vehicle.js +++ /dev/null @@ -1,625 +0,0 @@ -'use strict'; - -function drawCars() -{ - for(const v of vehicles) - v.draw(); -} - -function updateCars() -{ - // spawn in more vehicles - const playerIsSlow = titleScreenMode || playerVehicle.velocity.z < 20; - const trafficPosOffset = playerIsSlow? 0 : 16e4; // check in front/behind - const trafficLevel = (playerVehicle.pos.z+trafficPosOffset)/checkpointDistance; - const trafficLevelInfo = getLevelInfo(trafficLevel); - const trafficDensity = trafficLevelInfo.trafficDensity; - const maxVehicleCount = 10*trafficDensity; - if (trafficDensity) - if (vehicles.length!o.destroyed); -} - -function spawnVehicle(z) -{ - if (disableAiVehicles) - return; - - const v = new Vehicle(z); - vehicles.push(v); - v.update(); -} - -/////////////////////////////////////////////////////////////////////////////// - -class Vehicle -{ - constructor(z, color) - { - this.pos = vec3(0,0,z); - this.color = color; - this.isBraking = - this.drawTurn = - this.drawPitch = - this.wheelTurn = 0; - this.collisionSize = vec3(230,200,380); - this.velocity = vec3(); - - if (!this.color) - { - this.color = // random color - randInt(9) ? hsl(rand(), rand(.5,.9),.5) : - randInt(2) ? WHITE : hsl(0,0,.1); - - // not player if no color - //if (!isPlayer) - { - if (this.isTruck = randInt(2)) // random trucks - { - this.collisionSize.z = 450; - this.truckColor = hsl(rand(),rand(.5,1),rand(.2,1)); - } - - // do not pick same lane as player if behind - const levelInfo = getLevelInfo(this.pos.z/checkpointDistance); - this.lane = randInt(levelInfo.laneCount); - if (!titleScreenMode && z < playerVehicle.pos.z) - this.lane = playerVehicle.pos.x > 0 ? 0 : levelInfo.laneCount-1; - this.laneOffset = this.getLaneOffset(); - this.velocity.z = this.getTargetSpeed(); - } - } - } - - getTargetSpeed() - { - const levelInfo = getLevelInfo(this.pos.z/checkpointDistance); - const lane = levelInfo.laneCount - 1 - this.lane; // flip side - return max(120,120 + lane*20); // faster on left - } - - getLaneOffset() - { - const levelInfo = getLevelInfo(this.pos.z/checkpointDistance); - const o = (levelInfo.laneCount-1)*laneWidth/2; - return this.lane*laneWidth - o; - } - - update() - { - ASSERT(this != playerVehicle); - - // update ai vehicles - const targetSpeed = this.getTargetSpeed(); - const accel = this.isBraking ? (--this.isBraking, -1) : - this.velocity.z < targetSpeed ? .5 : - this.velocity.z > targetSpeed+10 ? -.5 : 0; - - const trackInfo = new TrackSegmentInfo(this.pos.z); - const trackInfo2 = new TrackSegmentInfo(this.pos.z+trackSegmentLength); - const level = this.pos.z/checkpointDistance | 0; - const levelInfo = getLevelInfo(level); - - { - // update lanes - this.lane = min(this.lane, levelInfo.laneCount-1); - //if (rand() < .01 && this.pos.z > playerVehicle.pos.z) - // this.lane = randInt(levelInfo.laneCount); - - // move into lane - const targetLaneOffset = this.getLaneOffset(); - this.laneOffset = lerp(.01, this.laneOffset, targetLaneOffset); - const lanePos = this.laneOffset; - this.pos.x = lanePos; - } - - // update physics - this.pos.z += this.velocity.z = max(0, this.velocity.z+accel); - - // slow down if too close to other vehicles - const x = this.laneOffset; - for(const v of vehicles) - { - // slow down if behind - if (v != this && v != playerVehicle) - if (this.pos.z < v.pos.z + 500 && this.pos.z > v.pos.z - 2e3) - if (abs(x-v.laneOffset) < 500) // lane space - { - if (this.pos.z >= v.pos.z) - this.destroyed = 1; // get rid of overlaps - this.velocity.z = min(this.velocity.z, v.velocity.z++); // clamp velocity & push - this.isBraking = 30; - break; - } - } - - // move ai vehicles - this.pos.x = trackInfo.pos.x + x; - this.pos.y = trackInfo.offset.y; - - // get projected track angle - const delta = trackInfo2.pos.subtract(trackInfo.pos); - this.drawTurn = Math.atan2(delta.x, delta.z); - this.wheelTurn = this.drawTurn / 2; - this.drawPitch = trackInfo.pitch; - - // remove in front or behind - const playerDelta = this.pos.z - playerVehicle.pos.z; - this.destroyed |= playerDelta > 7e4 || playerDelta < -2e3; - } - - draw() - { - const trackInfo = new TrackSegmentInfo(this.pos.z); - const vehicleHeight = 75; - const p = this.pos.copy(); - p.y += vehicleHeight; - p.z = p.z - cameraOffset; - - if (p.z < 0 && !freeCamMode) - { - // causes glitches if rendered - return; // behind camera - } - - /*{ // test cube - //p.y = trackInfo.offset.y; - const heading = this.drawTurn+PI/2; - const trackPitch = trackInfo.pitch; - const m2 = buildMatrix(p.add(vec3(0,-vehicleHeight,0)), vec3(trackPitch,0,0)); - const m1 = m2.multiply(buildMatrix(0, vec3(0,heading,0), 0)); - cubeMesh.render(m1.multiply(buildMatrix(0, 0, vec3(50,20,2e3))), this.color); - // return - }*/ - - // car - const heading = this.drawTurn; - const trackPitch = trackInfo.pitch; - - const carPitch = this.drawPitch; - const mHeading = buildMatrix(0, vec3(0,heading), 0); - const m1 = buildMatrix(p, vec3(carPitch,0)).multiply(mHeading); - const mcar = m1.multiply(buildMatrix(0, 0, vec3(450,this.isTruck?700:500,450))); - - { - // shadow - glSetDepthTest(this != playerVehicle,0); // no depth test for player shadow - glPolygonOffset(60); - const lightOffset = vec3(0,0,-60).rotateY(worldHeading); - const shadowColor = rgb(0,0,0,.5); - const shadowPosBase = vec3(p.x,trackInfo.pos.y,p.z).addSelf(lightOffset); - const shadowSize = vec3(-720,200,600); // why x negative? - - const m2 = buildMatrix(shadowPosBase, vec3(trackPitch,0)).multiply(mHeading); - const mshadow = m2.multiply(buildMatrix(0, 0, shadowSize)); - shadowMesh.renderTile(mshadow, shadowColor, spriteList.carShadow.spriteTile); - glPolygonOffset(); - glSetDepthTest(); - } - - carMesh.render(mcar, this.color); - //cubeMesh.render(m1.multiply(buildMatrix(0, 0, this.collisionSize)), BLACK); // collis - - let bumperY = 130, bumperZ = -440; - if (this.isTruck) - { - bumperY = 50; - bumperZ = -560; - const truckO = vec3(0,290,-250); - const truckColor = this.truckColor; - const truckSize = vec3(240,truckO.y,300); - glPolygonOffset(20); - cubeMesh.render(m1.multiply(buildMatrix(truckO, 0, truckSize)), truckColor); - } - glPolygonOffset(); // turn it off! - - if (optimizedCulling) - { - const distanceFromPlayer = this.pos.z - playerVehicle.pos.z; - if (distanceFromPlayer > 4e4) - return; // cull too far - } - - // wheels - const wheelRadius = 110; - const wheelSpinScale = 400; - const wheelSize = vec3(50,wheelRadius,wheelRadius); - const wheelM1 = buildMatrix(0,vec3(this.pos.z/wheelSpinScale,this.wheelTurn),wheelSize); - const wheelM2 = buildMatrix(0,vec3(this.pos.z/wheelSpinScale,0),wheelSize); - const wheelColor = hsl(0,0,.2); - const wheelOffset1 = vec3(240,25,220); - const wheelOffset2 = vec3(240,25,-300); - for (let i=4;i--;) - { - const wo = i<2? wheelOffset1 : wheelOffset2; - - glPolygonOffset(this.isTruck && i>1 && 20); - const o = vec3(i%2?wo.x:-wo.x, wo.y, i<2? wo.z : wo.z); - carWheel.render(m1.multiply(buildMatrix(o)).multiply(i<2 ? wheelM1 :wheelM2), wheelColor); - } - - // decals - glPolygonOffset(40); - - // bumpers - cubeMesh.render(m1.multiply(buildMatrix(vec3(0,bumperY,bumperZ), 0, vec3(140,50,20))), hsl(0,0,.1)); - - // break lights - const isBraking = this.isBraking; - for(let i=2;i--;) - { - const color = isBraking ? hsl(0,1,.5) : hsl(0,1,.2); - glEnableLighting = !isBraking; // make it full bright when braking - cubeMesh.render(m1.multiply(buildMatrix(vec3((i?1:-1)*180,bumperY-25,bumperZ-10), 0, vec3(40,25,5))), color); - glEnableLighting = 1; - cubeMesh.render(m1.multiply(buildMatrix(vec3((i?1:-1)*180,bumperY+25,bumperZ-10), 0, vec3(40,25,5))), WHITE); - } - - if (this == playerVehicle) - { - // only player needs front bumper - cubeMesh.render(m1.multiply(buildMatrix(vec3(0,10,440), 0, vec3(240,30,30))), hsl(0,0,.5)); - - // license plate - quadMesh.renderTile(m1.multiply(buildMatrix(vec3(0,bumperY-80,bumperZ-20), vec3(0,PI,0), vec3(80,25,1))),WHITE, spriteList.carLicense.spriteTile); - - // top number - const m3 = buildMatrix(0,vec3(0,PI)); // flip for some reason - quadMesh.renderTile(m1.multiply(buildMatrix(vec3(0,230,-200), vec3(PI/2-.2,0,0), vec3(140)).multiply(m3)),WHITE, spriteList.carNumber.spriteTile); - } - - glPolygonOffset(); - } -} - -/////////////////////////////////////////////////////////////////////////////// - -class PlayerVehicle extends Vehicle -{ - constructor(z, color) - { - super(z, color, 1); - this.playerTurn = - this.bumpTime = - this.onGround = - this.engineTime = 0; - this.hitTimer = new Timer; - } - - draw() { titleScreenMode || super.draw(); } - - update() - { - if (titleScreenMode) - { - this.pos.z += this.velocity.z = 20; - return; - } - - const playHitSound=()=> - { - if (!this.hitTimer.active()) - { - sound_hit.play(percent(this.velocity.z, 0, 50)); - this.hitTimer.set(.5); - } - } - - const hitBump=(amount = .98)=> - { - this.velocity.z *= amount; - if (this.bumpTime < 0) - { - sound_bump.play(percent(this.velocity.z, 0, 50)); - this.bumpTime = 500*rand(1,1.5); - this.velocity.y += min(50, this.velocity.z)*rand(.1,.2); - } - } - - this.bumpTime -= this.velocity.z; - - if (!freeRide && checkpointSoundCount > 0 && !checkpointSoundTimer.active()) - { - sound_checkpoint.play(); - checkpointSoundTimer.set(.26); - checkpointSoundCount--; - } - - const playerDistance = playerVehicle.pos.z; - if (!gameOverTimer.isSet()) - if (playerDistance > nextCheckpointDistance) - { - // checkpoint - ++playerLevel; - nextCheckpointDistance += checkpointDistance; - checkpointTimeLeft += extraCheckpointTime; - if (enhancedMode) - checkpointTimeLeft = min(60,checkpointTimeLeft); - - if (playerLevel >= levelGoal && !gameOverTimer.isSet()) - { - // end of game - playerWin = 1; - sound_win.play(); - gameOverTimer.set(); - if (!(debug && debugSkipped)) - if (!freeRide) - { - bestDistance = 0; // reset best distance - if (raceTime < bestTime || !bestTime) - { - // new fastest time - bestTime = raceTime; - playerNewRecord = 1; - } - writeSaveData(); - } - } - else - { - //speak('CHECKPOINT'); - checkpointSoundCount = 3; - } - } - - // check for collisions - if (!testDrive) - for(const v of vehicles) - { - const d = this.pos.subtract(v.pos); - const s = this.collisionSize.add(v.collisionSize); - if (v != this && abs(d.x) < s.x && abs(d.z) < s.z) - { - // collision - const oldV = this.velocity.z; - this.velocity.z = v.velocity.z/2; - //console.log(v.velocity.z, oldV*.9); - v.velocity.z = max(v.velocity.z, oldV*.9); // push other car - this.velocity.x = 99*sign(d.x); // push away from car - playHitSound(); - } - } - - // get player input - let playerInputTurn = keyIsDown('ArrowRight') - keyIsDown('ArrowLeft'); - let playerInputGas = keyIsDown('ArrowUp'); - let playerInputBrake = keyIsDown('Space') || keyIsDown('ArrowDown'); - - if (isUsingGamepad) - { - playerInputTurn = gamepadStick(0).x; - playerInputGas = gamepadIsDown(0) || gamepadIsDown(7); - playerInputBrake = gamepadIsDown(1) || gamepadIsDown(2) || gamepadIsDown(3) || gamepadIsDown(6); - - const analogGas = gamepadGetValue(7); - if (analogGas) - playerInputGas = analogGas; - const analogBrake = gamepadGetValue(6); - if (analogBrake) - playerInputBrake = analogBrake; - } - - if (playerInputGas) - mouseControl = 0; - if (debug && (mouseWasPressed(0) || mouseWasPressed(2) || isUsingGamepad && gamepadWasPressed(0))) - testDrive = 0; - - if (mouseControl || mouseIsDown(0)) - { - mouseControl = 1; - playerInputTurn = clamp(5*(mousePos.x-.5),-1,1); - playerInputGas = mouseIsDown(0); - playerInputBrake = mouseIsDown(2); - - if (isTouchDevice && mouseIsDown(0)) - { - const touch = 1.8 - 2*mousePos.y; - playerInputGas = percent(touch, .1, .2); - playerInputBrake = touch < 0; - playerInputTurn = clamp(3*(mousePos.x-.5),-1,1); - } - } - if (freeCamMode) - playerInputGas = playerInputTurn = playerInputBrake = 0; - if (testDrive) - playerInputGas = 1, playerInputTurn=0; - if (gameOverTimer.isSet()) - playerInputGas = playerInputTurn = playerInputBrake = 0; - this.isBraking = playerInputBrake; - - const sound_velocity = max(40+playerInputGas*50,this.velocity.z); - this.engineTime += sound_velocity*sound_velocity/5e4; - if (this.engineTime > 1) - { - if (--this.engineTime > 1) - this.engineTime = 0; - const f = sound_velocity; - sound_engine.play(.1,f*f/4e3+rand(.1)); - } - - const playerTrackInfo = new TrackSegmentInfo(this.pos.z); - const playerTrackSegment = playerTrackInfo.segmentIndex; - - // gravity - const gravity = -3; // gravity to apply in y axis - this.velocity.y += gravity; - - // player settings - const forwardDamping = .998; // dampen player z speed - const lateralDamping = .5; // dampen player x speed - const playerAccel = 1; // player acceleration - const playerBrake = 2; // player acceleration when braking - const playerMaxSpeed = 200; // limit max player speed - const speedPercent = this.velocity.z/playerMaxSpeed; - const centrifugal = .5; - - // update physics - const velocityAdjusted = this.velocity.copy(); - const trackHeadingScale = 20; - const trackHeading = Math.atan2(trackHeadingScale*playerTrackInfo.offset.x, trackSegmentLength); - const trackScaling = 1 / (1 + (this.pos.x/(2*laneWidth)) * Math.tan(-trackHeading)); - velocityAdjusted.z *= trackScaling; - this.pos.addSelf(velocityAdjusted); - - // clamp player x position - const maxPlayerX = playerTrackInfo.width + 500; - this.pos.x = clamp(this.pos.x, -maxPlayerX, maxPlayerX); - - // check if on ground - const wasOnGround = this.onGround; - this.onGround = this.pos.y < playerTrackInfo.offset.y; - if (this.onGround) - { - this.pos.y = playerTrackInfo.offset.y; - const trackPitch = playerTrackInfo.pitch; - this.drawPitch = lerp(.2,this.drawPitch, trackPitch); - - // bounce off track - const trackNormal = vec3(0, 1, 0).rotateX(trackPitch); - const elasticity = 1.2; - const normalDotVel = this.velocity.dot(trackNormal); - const reflectVelocity = trackNormal.scale(-elasticity * normalDotVel); - - if (!gameOverTimer.isSet()) // dont roll in game over - this.velocity.addSelf(reflectVelocity); - if (!wasOnGround) - { - const p = percent(reflectVelocity.length(), 20, 80); - sound_bump.play(p*2,.5); - } - - const trackSegment = track[playerTrackSegment]; - if (trackSegment && !trackSegment.sideStreet) // side streets are not offroad - if (abs(this.pos.x) > playerTrackInfo.width - this.collisionSize.x && !testDrive) - hitBump(); // offroad - - // update velocity - if (playerInputBrake) - this.velocity.z -= playerBrake*playerInputBrake; - else if (playerInputGas) - { - // extra boost at low speeds - //const lowSpeedPercent = this.velocity.z**2/1e4; - const lowSpeedPercent = percent(this.velocity.z, 150, 0)**2; - const accel = playerInputGas*playerAccel*lerp(speedPercent, 1, .5) - * lerp(lowSpeedPercent, 1, 3); - //console.log(lerp(lowSpeedPercent, 1, 9)) - - // apply acceleration in angle of road - //const accelVec = vec3(0,0,accel).rotateX(trackSegment.pitch); - //this.velocity.addSelf(accelVec); - this.velocity.z += accel; - } - else if (this.velocity.z < 30) - this.velocity.z *= .9; // slow to stop - - // dampen z velocity & clamp - this.velocity.z = max(0, this.velocity.z*forwardDamping); - this.velocity.x *= lateralDamping; - } - else - { - // in air - this.drawPitch *= .99; // level out pitch - this.onGround = 0; - } - - { - // turning - let desiredPlayerTurn = startCountdown ? 0 : playerInputTurn; - if (testDrive) - { - desiredPlayerTurn = clamp(-this.pos.x/2e3, -1, 1); - this.pos.x = clamp(this.pos.x, -playerTrackInfo.width, playerTrackInfo.width); - } - - // scale desired turn input - desiredPlayerTurn *= .4; - const playerMaxTurnStart = 50; // fade on turning visual - const turnVisualRamp = clamp(this.velocity.z/playerMaxTurnStart,0,.1); - this.wheelTurn = lerp(.1, this.wheelTurn, 1.3*desiredPlayerTurn); - this.playerTurn = lerp(.05, this.playerTurn, desiredPlayerTurn); - this.drawTurn = lerp(turnVisualRamp, this.drawTurn, this.playerTurn); - - // centripetal force - const centripetalForce = -velocityAdjusted.z * playerTrackInfo.offset.x * centrifugal; - this.pos.x += centripetalForce - - // apply turn velocity and slip - const physicsTurn = this.onGround ? this.playerTurn : 0; - const maxStaticFriction = 30; - const slip = maxStaticFriction/max(maxStaticFriction,abs(centripetalForce)); - - const turnStrength = .8; - const turnForce = turnStrength * physicsTurn * this.velocity.z; - this.velocity.x += turnForce*slip; - } - - if (playerWin) - this.drawTurn = lerp(gameOverTimer.get(), this.drawTurn, -1); - if (startCountdown) - this.velocity.z = 0; // wait to start - if (gameOverTimer.isSet()) - this.velocity = this.velocity.scale(.95); - - if (!testDrive) - { - // check for collisions - const collisionCheckDistance = 20; // segments to check - for(let i = -collisionCheckDistance; i < collisionCheckDistance; ++i) - { - const segmentIndex = playerTrackSegment+i; - const trackSegment = track[segmentIndex]; - if (!trackSegment) - continue; - - // collidable objects - for(const trackObject of trackSegment.trackObjects) - { - if (!trackObject.collideSize) - continue; - - // check for overlap - const pos = trackSegment.offset.add(trackObject.offset); - const dp = this.pos.subtract(pos); - const csx = this.collisionSize.x+trackObject.collideSize; - if (abs(dp.z) > 430 || abs(dp.x) > csx) - continue; - - if (trackObject.sprite.isBump) - { - trackObject.collideSize = 0; // prevent colliding again - hitBump(.8); // hit a bump - } - else if (trackObject.sprite.isSlow) - { - trackObject.collideSize = 0; // prevent colliding again - sound_bump.play(percent(this.velocity.z, 0, 50)*3,.2); - // just slow down the player - this.velocity.z *= .85; - } - else - { - // push player away - const onSideOfTrack = abs(pos.x)+csx+200 > playerTrackInfo.width; - const pushDirection = onSideOfTrack ? - -pos.x : // push towards center - dp.x; // push away from object - - this.velocity.x = 99*sign(pushDirection); - this.velocity.z *= .7; - playHitSound(); - } - } - } - } - } -} \ No newline at end of file diff --git a/vue/public/race/webgl.js b/vue/public/race/webgl.js deleted file mode 100644 index bc5844d..0000000 --- a/vue/public/race/webgl.js +++ /dev/null @@ -1,305 +0,0 @@ -'use strict'; - -/* - -Small and fast dynamic webgl rendering engine for Dr1v3n Wild - -Features -- batch rendering -- direct and ambient lighting -- fog with alpha blending -- texture mapping -- vertex color - -Potential improvements -- everything is using dynamic buffer, which is slow but flexible -- it would be faster to use static buffers for static geometry -- the colors could be passed in as 32 bit integers rather then vec4s -- specular lighting would also be pretty easy to include -- the fog calculation could possibly be moved to the vertex shader -- a mip map of the passed in texture could be auto generated for smoother scaling -- additive blending would also be easy to implement -- there should be an easier way to set the fog range - -*/ - -const glRenderScale = 100; // fixes floating point issues on some devices -const glSpecular = 0; // experimental specular test -let glCanvas, glContext, glShader, glVertexData; -let glBatchCount, glBatchCountTotal, glDrawCalls; -let glEnableLighting, glLightDirection, glLightColor, glAmbientColor; -let glEnableFog, glFogColor; - -/////////////////////////////////////////////////////////////////////////////// -// webgl setup - -function glInit() -{ - // create the canvas - const hasAlpha = false; // there should be no alpha for the background texture - document.body.appendChild(glCanvas = document.createElement('canvas')); - glContext = glCanvas.getContext('webgl2', {alpha: hasAlpha}); - ASSERT(glContext, 'Failed to create WebGL canvas!'); - - // setup vertex and fragment shaders - glShader = glCreateProgram( - '#version 300 es\n' + // specify GLSL ES version - 'precision highp float;'+ // use highp for better accuracy - 'uniform vec4 l,g,a,f;' + // light direction, color, ambient light, fog - 'uniform mat4 m,o;'+ // projection matrix, object matrix - 'in vec4 p,n,u,c;'+ // in: position, normal, uv, color - 'out vec4 v,d,q;'+ // out: uv, color, fog - 'void main(){'+ // shader entry point - 'gl_Position=m*o*p;'+ // transform position - 'v=u,q=f;'+ // pass uv and fog to fragment shader - 'd=c*vec4(a.xyz+g.xyz*max(0.,dot(l.xyz,'+ // lighting - 'normalize((transpose(inverse(o))*n).xyz))),1);' + // transform light - '}' // end of shader - , - '#version 300 es\n' + // specify GLSL ES version - 'precision highp float;'+ // use highp for better accuracy - 'in vec4 v,d,q;'+ // uv, color, fog - 'uniform sampler2D s;'+ // texture - 'out vec4 c;'+ // out color - 'void main(){'+ // shader entry point - 'c=v.z>0.?d:texture(s,v.xy)*d;'+ // color or texture - 'float f=gl_FragCoord.z/gl_FragCoord.w;'+ // fog depth - 'v.w>0.?c:c=vec4(mix(c.xyz,q.xyz,clamp(f*f/1e10,0.,1.)),'+ // fog color - 'c.a*clamp(4.-f/2e4,0.,1.));'+ // fog alpha - //'c.w);'+ // disable fog alpha - //'if (c.a == 0.) discard;'+ // discard if no alpha - '}' // end of shader - ); - - // set up the shader - glContext.useProgram(glShader); - glContext.bindBuffer(gl_ARRAY_BUFFER, glContext.createBuffer()); - glContext.bufferData(gl_ARRAY_BUFFER, gl_VERTEX_BUFFER_SIZE, gl_DYNAMIC_DRAW); - glContext.blendFunc(gl_SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA); - glSetCapability(gl_BLEND); - glSetCapability(gl_CULL_FACE); // not culling causeses thin black lines sometimes - glVertexData = new Float32Array(new ArrayBuffer(gl_VERTEX_BUFFER_SIZE)); - - // set vertex attributes - let offset = 0; - const vertexAttribute = (name)=> - { - const type = gl_FLOAT, stride = gl_VERTEX_BYTE_STRIDE; - const size = 4, byteCount = 4; - const location = glContext.getAttribLocation(glShader, name); - glContext.enableVertexAttribArray(location); - glContext.vertexAttribPointer(location, size, type, 0, stride, offset); - offset += size*byteCount; - } - vertexAttribute('p'); // position - vertexAttribute('n'); // normal - vertexAttribute('u'); // uv - vertexAttribute('c'); // color -} - -function glCompileShader(source, type) -{ - // build the shader - const shader = glContext.createShader(type); - glContext.shaderSource(shader, source); - glContext.compileShader(shader); - - // check for errors - if (debug && !glContext.getShaderParameter(shader, gl_COMPILE_STATUS)) - throw glContext.getShaderInfoLog(shader); - return shader; -} - -function glCreateProgram(vsSource, fsSource) -{ - // build the program - const program = glContext.createProgram(); - glContext.attachShader(program, glCompileShader(vsSource, gl_VERTEX_SHADER)); - glContext.attachShader(program, glCompileShader(fsSource, gl_FRAGMENT_SHADER)); - glContext.linkProgram(program); - - // check for errors - if (debug && !glContext.getProgramParameter(program, gl_LINK_STATUS)) - throw glContext.getProgramInfoLog(program); - return program; -} - -function glCreateTexture(image) -{ - // build the texture - const texture = glContext.createTexture(); - glContext.bindTexture(gl_TEXTURE_2D, texture); - glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image); - return texture; -} - -function glPreRender(canvasSize) -{ - // set size of canvas and viewport which also clears it - glContext.viewport(0, 0, glCanvas.width = canvasSize.x, glCanvas.height = canvasSize.y); - glDrawCalls = glBatchCount = glBatchCountTotal = 0; // reset draw counts - //debug && glContext.clearColor(1, 0, 1, 1); // test background color - //glContext.clear(gl_DEPTH_BUFFER_BIT|gl_COLOR_BUFFER_BIT); // auto cleared - - // use point filtering for pixelated rendering - glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, gl_NEAREST); - glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, gl_NEAREST); - - // set up the camera transform - const viewMatrix = buildMatrix(cameraPos, cameraRot).inverse(); - const combinedMatrix = glCreateProjectionMatrix().multiply(viewMatrix); - glContext.uniformMatrix4fv(glUniform('m'), 0, combinedMatrix.toFloat32Array()); -} - -function glRender(transform=new DOMMatrix) -{ - // set up the lights and fog - const initUniform4f = (name, x, y, z)=> glContext.uniform4f(glUniform(name), x, y, z, 0); - const lightColor = glEnableLighting ? glLightColor : BLACK; - const ambientColor = glEnableLighting ? glAmbientColor : WHITE; - initUniform4f('g', lightColor.r, lightColor.g, lightColor.b); - initUniform4f('a', ambientColor.r, ambientColor.g, ambientColor.b); - initUniform4f('f', glFogColor.r, glFogColor.g, glFogColor.b); - initUniform4f('l', glLightDirection.x, glLightDirection.y, glLightDirection.z); - - // render the verts - ASSERT(glBatchCount < gl_MAX_BATCH, 'Too many points!'); - const vertexData = glVertexData.subarray(0, glBatchCount * gl_INDICIES_PER_VERT); - const m = transform.scaleSelf(glRenderScale, glRenderScale, glRenderScale); - glContext.uniformMatrix4fv(glUniform('o'), 0, m.toFloat32Array()); - glContext.bufferSubData(gl_ARRAY_BUFFER, 0, vertexData); - glContext.drawArrays(gl_TRIANGLE_STRIP, 0, glBatchCount); - glBatchCountTotal += glBatchCount; - glBatchCount = 0; - ++glDrawCalls; -} - -/////////////////////////////////////////////////////////////////////////////// -// webgl helper functions - -const glUniform = (name) => glContext.getUniformLocation(glShader, name); - -function glSetCapability(cap, enable=1) -{ enable ? glContext.enable(cap) : glContext.disable(cap); } - -function glPolygonOffset(units=0) -{ glContext.polygonOffset(0, -units); glSetCapability(gl_POLYGON_OFFSET_FILL, !!units); } - -function glSetDepthTest(depthTest=1, depthWrite=1) -{ glSetCapability(gl_DEPTH_TEST, !!depthTest); glContext.depthMask(!!depthWrite); } - -function glCreateProjectionMatrix(fov=.5, near = 1, far = 1e4) -{ - const aspect = glCanvas.width / glCanvas.height; - const f = 1 / Math.tan(fov), range = far - near; - return new DOMMatrix - ([ - f / aspect, 0, 0, 0, - 0, f, 0, 0, - 0, 0, (near + far) / range, 2 * near * far / range, - 0, 0, -1, 0 - ]); -} - -/////////////////////////////////////////////////////////////////////////////// -// drawing functions - -const vectorOne = vec3(1); // no lighting/texture - -// push a list of colored verts with optonal normals and uvs -function glPushVerts(points, normals, color, uvs) -{ - const count = points.length; - if (!(count < gl_MAX_BATCH - glBatchCount)) - glRender(); - - const na = vectorOne; // no lighting/texture - for(let i=count; i--;) - glPushVert(points[i], normals ? normals[i] : na, uvs ? uvs[i] : na, color); -} - -// push a list of colored verts with optonal normals and uvs -// this is also capped with degenerate verts to close the shape -function glPushVertsCapped(points, normals, color, uvs) -{ - // push points with extra degenerate verts to cap both sides - const count = points.length; - if (!(count+2 < gl_MAX_BATCH - glBatchCount)) - glRender(); - - const na = vectorOne; // no lighting/texture - glPushVert(points[count-1], na, na, color); - for(let i=count; i--;) - glPushVert(points[i], normals ? normals[i] : na, uvs ? uvs[i] : na, color); - glPushVert(points[0], na, na, color); -} - -// push a list of colored verts without normals or uvs -function glPushColoredVerts(points, colors) -{ - // push points with a list of vertex colors - const count = points.length; - if (!(count+2 < gl_MAX_BATCH - glBatchCount)) - glRender(); - - const na = vectorOne; // no lighting/texture - glPushVert(points[count-1], na, na, colors[count-1]); - for(let i=count; i--;) - glPushVert(points[i], na, na, colors[i]); - glPushVert(points[0], na, na, colors[0]); -} - -// push a single vert to the buffer -function glPushVert(pos, normal, uv, color) -{ - let offset = glBatchCount++ * gl_INDICIES_PER_VERT; - glVertexData[offset++] = pos.x/glRenderScale; - glVertexData[offset++] = pos.y/glRenderScale; - glVertexData[offset++] = pos.z/glRenderScale; - glVertexData[offset++] = 1; - glVertexData[offset++] = normal.x; - glVertexData[offset++] = normal.y; - glVertexData[offset++] = normal.z; - glVertexData[offset++] = 0; - glVertexData[offset++] = uv.x; - glVertexData[offset++] = uv.y; - glVertexData[offset++] = uv.z; // >0 if untextured - glVertexData[offset++] = !glEnableFog; - glVertexData[offset++] = color.r; - glVertexData[offset++] = color.g; - glVertexData[offset++] = color.b; - glVertexData[offset++] = color.a; -} - -/////////////////////////////////////////////////////////////////////////////// -// store webgl constants as integers so they can be minifed - -const -gl_TRIANGLE_STRIP = 5, -gl_DEPTH_BUFFER_BIT = 256, -gl_SRC_ALPHA = 770, -gl_ONE_MINUS_SRC_ALPHA = 771, -gl_CULL_FACE = 2884, -gl_DEPTH_TEST = 2929, -gl_BLEND = 3042, -gl_TEXTURE_2D = 3553, -gl_UNSIGNED_BYTE = 5121, -gl_FLOAT = 5126, -gl_RGBA = 6408, -gl_NEAREST = 9728, -gl_TEXTURE_MAG_FILTER = 10240, -gl_TEXTURE_MIN_FILTER = 10241, -gl_COLOR_BUFFER_BIT = 16384, -gl_POLYGON_OFFSET_FILL = 32823, -gl_ARRAY_BUFFER = 34962, -gl_DYNAMIC_DRAW = 35048, -gl_FRAGMENT_SHADER = 35632, -gl_VERTEX_SHADER = 35633, -gl_COMPILE_STATUS = 35713, -gl_LINK_STATUS = 35714, - -// constants for batch rendering -gl_MAX_BATCH = 2e4, // max verts per batch -gl_INDICIES_PER_VERT = (1 * 4) * 4, // vec4 * 4 -gl_VERTEX_BYTE_STRIDE = gl_INDICIES_PER_VERT * 4, // 4 bytes per float -gl_VERTEX_BUFFER_SIZE = gl_MAX_BATCH * gl_VERTEX_BYTE_STRIDE; \ No newline at end of file diff --git a/vue/public/t_race/favicon.png b/vue/public/t_race/favicon.png deleted file mode 100644 index 17aaf4088c4df832337873f12898e6f458edfcdc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10912 zcmeHtXH=8hwsz>f2_hgR6r~12LJ7S~kxpnLB|t!WOz24Oy$I5zNfD5aB2|&zK>?8> zy@`MnX(Di=d!KXm9^d_Q#`x~PoiX0z&05cV=A6%(Yvo;QBwANnm4cLs6aWBFsH-XK zV?TkvE@DFLJI2@dI{o6!#*Q0F+Kg)5qZ%+?4L2r-@Jsr^G-ZUo!|PVnsI*NWXbLDEpfcsWT7xn z>tN#?UYm;)?4kDjzQ5!Zmq(#|!F7X3Nb;?c0Qn>96^}Q@`akPP2+y)}-n^BRlALk; z@im{#;w)h8D&X_Qmr6QWrDe7|f(wCHzMs#2dR5;aziL1`<{2+-cw~PgJNb$5zj$i1 zGY7@QB$Jj1<|+A`X$diin=0NF@BKJnaMrJ>_swggg=Uhhdb&Qh|K#8^(VWMlgY`(c zfQ7e%LJb)mo&^USVb_=2K3P-92()hOGc8<(#ZXbWMKo5;8jvse?R!mM3V0biRXFDs zyn|+La9;hGy_ltXS_t2}bXaJxwq)=7J%3j2UO=C9&1VnE={acRQ%;GEVEd&2hJ!XxNragfr|xjmI?}P3c7p zWzTJ;7+XCG&;tiq{R?)jt{8k_l3kkM28tg)WV^0+zcrs(iI$7|Z-_2&~<*i7_p(PJHn zqnG2Or^hqBl~<>iH!Kd>s>8qXQdBHhtT?PLG`cf^WC5xQ;_w`(&6pH z988bnnI_|94;^Is_>fbT>?p}z4tfc;vZifZS_WW5FZRq%-};X9dt80MTz#KiXtjG* z;30EtR}l9@xo&ie*d@$CG}nZ+?L&CZNX<-+oCE9a%HCy3e%v^g9`$eej9y(~;j^*-A}&N>ov;eGHKhu0fq*NT7-{k$pJQ5ZUd~|(*Ea``qp5m7_^&b7 ziU=w(OW1xk(G?0P?}>j&$T}sQEIj+jg8RnX;qdMym{XkJ1k!I-7CCCYvhDCSbuX(? z#vXTuF;Yhd29`V?r|?s83T0tQRIszpt-t$9HAljlx+zHg)7h@Qq9?v zGa-wW-bwo~r;PB;H&>SR1ad;BbSi7S)959>3d&Fm&_s)SZ3+p=M$Rh3_{o)xu)cX zQ19LCxRzV@?3QU!7)qNo@(CJyp6CEH_d7?ufVDJ7=+p(>x_hx~+r*M91g9nS~PWJo_wzGi?;TR@Z`LoVW*K)y>+~4 zX11+*RZFt?)D!bOii@6O595yzJo1pZ0h$enr;P9VyzSIz!ecF}f~itu#GbGMM8=&YB8Bs=ruaAx-mhU4^u(P$&wow` z(VsDw`{#FWYMB&!z^r)#l z+coa4bc!Dt(NoA(2DY{=Bz+W>@mZiVFG?W#gs)(Ir=68;c!>gF#>_@sk;yDQ@XiOb zSLUPT%pPf3&go8kU9N?5Vac?+W4Q%4Qj8?v8OmN*rJoT1x!*+tBfsSGN$~%K3#HR{ zcQsBr#yg)h*SI57|2eE_7g#g+tVAk>hL|M&;2Y4c(a4{D!TYD5VEU#CDytl@<&ne*_z3Qf?GM-%m^2k zoK#t<`HFA`!n2P<12uE7tqO(}^iMe+S?+FujNv9cJ!4+S^|{qycwf%fw}`3WO(Ls< zTgFL12Xx3a;zZ?l@ugdQ04JW};tH;0jcjgTwEJS`ZfsCoO)u?rY0KWi7ba0Tq;ouJ z`Q7-+kYsFh!{rl z!!;}O&Ni-2StBv4KsPmn4~Ym;j%XhRKsE(t%r2Qd?11|D_`6I^T_#YLj1tW&GL8gZ#K0L)q_F zW-6CzgEiHO{WxeQy1(1JBzf5*lt}?r%oy+d&bo z;5-(7mUO6)a_zdy*h?HQJ&hpn5Vc5x^N_2yt%Mg*`ARp5zCwV_&En&On<4Elp5$z4injIKUg0Honk~%5G3z4$uE!ph%I9fFcDD*FDEAq#|3EL_9z9gWz49iS^ zG)pYica6hP-)r|SnjdJ@op5APscd2yAAHJv12(r@n6`<@_r2!Vp6His0Vfk*dv3eR zz4C%*EDBXb>Ja4}RS>|8lKd3!iLr#|ypg^>Gm;3;IdJ9}&nh}TFFUvUF7@@C5beW_ zlHnzFCmUO3dtXdmo}}PaJzpm6%n^z4F=0Hv zAzyfZzt)o!_q~?uTjHr&&C$J?>eA#m8cp$XKawmr%2S`y4PT4uOE9K(a~=Z^+BHoHc<|{eelYb0bG!+N^f!ZN)56DSydTFtmk^!> zg*xyWp{(2?y{Ba(IVACso_!Qi;!R`YiQ}nYxce$@i!}+^zG%wbgt~Hm}m6ZClQ*NdgNBgAWI-MN%)<~7-NX2|&tNtQu zkTtaNz2QpC+X);)mOmS^IgE6u-S`PcwB2^*GWOG1DM{$iiD(YYm9ZcTo>M6n>1Nm- zWcC`_I1}Y3uW6&!R|tzowstH`3G4{exRh@&+LaH&U{+5JO0!wiL&`kiHOGD$kzd%UU-40m8lIeyvWZ2rN)lWDVF)VHQbW*8>SXB>0Xw)@ULWbAKFC zKwS)%7iKK>(?sbj`q_WxODzJBdzV9EC!D-t20(pw_mQW{x9H##%8FcERt|kplr^5_ zveviqc=3d1d2Q7y)@!oe6%=m?B#7C@Kg^|#>}IvpO4U1QjE`$pVVv-dk>-+_9?MX3 zo_u*nJ+ymbwFKNmkZD`nl~&=a%-7@a+Qhk>Cxk`1T&Og&L&1>NEs}l=&`ug1y@C$Z z9o+&wdQE?ddT%-=^xBpKlAoYoTD!adKJ}<^Pc7zqE_{hoXJWxrg=lFr$`l=Xt|#c4 zeN)E#q>9>dQmID&^9{#jeqZSz(sAQSl~2Z#&aMI7uT4|lHj|<1=q01+qWDnINTQ*S zzH>{MiK4efSH&Iut)%4lKW@!Zm#6EpQgYBQ5Cu0`ldDNLT)pEY^=?;dtA2nle&j1_ zLG!4IQ6Vggy@xt@c3IyGU?LYDWw2L@>Z9wm8|~8~NMh1t-090Qr&GA45x3g3C(cOd z`x2ltcR9O8Sto^rgh@6xe_KA(Q!>?G)6-W~Mo|>~G!Er+B@g>4&&tK?{Xuip7-;5U z6yM=ihHNwhsc{1AujPvBMKi(q*uKdqzv9b_&Vv=dEPdIiBoRxb7P`an!2S3#f&iR) zCjaBonNNn-aqUZT&b#qJ#~>LJe0 z7Kot-Hpems-{JDdbS(ULqqs5De&>tA*F$8B>xPw-w%?`PBe@HR98j3K{l#% zpz9gQUe=2;$#POC{IS^gkAw!cA=U(kN$tek9y=Ug77V{9Eo_lhyw_z+6_eM`+fC0+ zd3rakFLk7Oa_l)mDWB!lpeKAGxZP+Y*d~g4g56fG97wd!QPCr3zC>Za*e5N>s0D_p z$Jf0A$d`9T(3cEoq!E8$1Z((bnIX+zmu@%t>k!|Wo}RR9DmTe~*ZPozQ>9_`B=&7B zi9ySJhEk+8Gu_+_zqmTBjum{ewad@QMd85>zX`VKlyt(wHjVf?-<&BTx&VGuTbm1W z7-Y>|_jyyP+nfihLyu}1Wk`@4iGCK#u8(f)YaC|Y*e5S8XgW$+psvsxEtL7+BQ}Nw zLxz&vs`K9--`7dn4pSQ&G5DPPlzDcTL6`X+4ZWHeDl1FRU>*{rE6-A%54@OtGkE(+?lpqcSDFDyR%n$cSouOJC(yS8t4TO5J0h)Jw8`-Z}UhPfWScfQqd& zZtv}U8DEDmU3_a;IIeKAa)YR^HdhyuZy(fHAi)Fh`P`Y4o=*6o??mu{ZDmoXSGZ%! zV5pz7OUKV4ZiTAI)@^Xp>~7M5w*EZKsz;S8@#_XCM2!hd4nnadwt6E|pn5ry9ba6+ zCHUApv^rIj)V&(aqG}A+iUxk16cDONrT3BO=)L%Afui**b&Jf|=3=#LN;a@axgBZR zLX4mA!&gn@C`5Jt@Jl(b>ydvs9hL3iYNA0l9_Gc){AAYl$q0cBi74gV1gMxf&Y)6F z=7!#Q?hXz(JX(nJjxnKcPHQEZnymaOZbzIxm@%HZp&!v(%^eSOIq(we~r z2PJQDH+EWk5T6+`%Xe&>Qr%TcKIAx@WO%^$s*w1{56;bZg{2*tGh`y|vT()*J8Jf! z#0ffeEMbuk~Pf+`Q=LdK0*8CldOBj?J43G3cME=%tzNzM3U(yRlcRsXg0xn zZb#@_{jq}w=S3J9u#o<~OBkp1yTH1~i?^Z=zn!&OxWNFl)CCOVSBYmr>9aRr>?m*o z0Du$jproX$uB7y@qb2Nt(o_Fr8MRJ%=5EVz4MRRkPu+)ox;X+dF))qmu~r2yuix_k zH+@%n@FA(F5k^`?KqN>1f>Lm#S+bEdWRWPUKJ@Xk*woXvMPnR2p(_EcEhmkBM8roB zgW+~qzT=bRnBFdDR~7O%&&cqIt;lhrGkrPQl7cr*nY! zmfWP@m)!qeQjD4BrmRmXqRyjDWO7$nR;?NoeEU#PQebbWCRu%$CNjF)`sSd^kVb@t zv>=1G)7j zHusF9i0B5f>pDw#Zz@FUc=StIzr>Mt@!uc!R4RR5|i-KdN)5j;ZF5=pND=4b? z)z36Z@S(bIWwA;^(7emya&vLNU;7&6*j3Tph*h2&-IVnF=jr*`xAs{V7XSod{Pl(cbk5k%O! zStA9#T~OGwKmb5m&KrfWaYDKSt&#Q)t}-0oTHbO19c*Pdj77D;S|}yt0|zxFF`M;pc~p=NLWHb zLI?~IfW=URxnekfQT)N7jKtWW9Z>ELZmz&zoCs?-4|f?34s1X0U-7x1 zw6y+B?~3_T1*{%I-UyVCupn5-#YO0^78rLGPb|rw0sXHQ7&!LGR7f9*aq~diAXPk( zuI`+FrLeX6w>`=O?fg3(TN@#yGtvbM#bCV(|IMYUx|Z(0Eq+O0@8E*^ZG{#4Z%B6s zyZ?ywx7dF5{7&bufnd%5&HFd>zjFT##-g;eB$eH4Jbs0zt}Mgx%fFvKoAm830ph6zfkC)9k5l2aQ%Mm@rZli4ca`+5D!mwUNBzhIT<#t1JrX;^|17D0wLAQ8!-cSwkoYC9FjQ0wB!aLN!^(>k1KEl}M8)kykWdLI zJP1RAMekIfXD4Yq`SX9MKqF?(62fDuWY4-{vSQ0e*^xq2w?60Xu~cp*tJUN-;33sdi|2{|M2zaIQ$=az*7I? z%UUqzXJbLUH{1SUn%fkf&Zzl|8H`U{%5;`bj3aad0{s*6iky3u^S;m z>$|GTfOEhVcDE|GJCAK4L8+Ny001)DUl$G_>nQ`ak;q+LOND5Ygo#3mhoBS!$HG|D zl@;J7oyF-+D8tF?y@5{O{72f`pd2r6Tc3a z?7eYCFmG9~mHr&Hp0;^?%9M^IUJ(08ArtIqcn!an9F2SJ2P164tNur&Dz9b^j=Tqn z_KzA06j!QVxZ^AMxw!xf6vtjoDC?IaQrT~}36mG7CpYeuzsw)$T zxsylKJ`q2tixb!m#aoVfZyjyD_VdK}V6G9$Fk#9ej$keppYLnGK9&4Lt~(}pEQ%?y z6gAwS1BgwH?fb5E_vm;X^Fsx(zK#UUeSCDBUT!t>Bt8Gy1JwkxY)U1#8b|DSs_=U? zPf%7*l6w88q+R*l7RC?);|QP?2em?V&Tw4fg3|8v!86=OD`Jkc2PW;DLDh?#qSNNG#j>p_f%6G4^yq?-Rq{xYdGFGV8}qG* zxAD;g?aa0#NmP)dd{;iPLfr*5?vtn&Oxejz;{`>H!9>E}n->hooLdj|8N>u$lwd=W z*@z6!u>9F%8{w?(bBa!})#?VjN%^M;qb#kK0+FjK2sZe84Z_U0NKH{=~{ z4HF)YaLkQLctrQe#jVcj?B0rvt)tP5PFQ@EHq8jCVy5jE$E>xiI7qK$@uo>&ewnemB4MQBTgzsLnHI;PwB;r z88#8e&}Q5EnvF;KAWJ24{UXltXSFS3-iRO5rbvPVE1WX-6&-~ zAB-~D6+KCiMGZtGma$inOq>Rj6F<451ygYHZfVSX9XsZ63U>65R>EQQzzKaVN7W6f z&rS#{=}fknY8%VOrR4~@YYOwED{hBZj2 z7p0ZW&uDzRt1wtoO{`8bV9tDCi;UUgt7)ZY3Z|fvBW{nndwYfHkvM?j%}r4nGEe9H z=?SnlC6adM(eukFa)H|+!-H0VjE;QLb_4m{&Z(P%@7{oKRle_Jqse#sbmrx z4vhL(cKDzjsuqTGLuX)Qhh+5Bn>w`8=Cw|yt{i>{GDL!*-oYWf$(k6+pc^rkR%@sq zF(NbeifTZGGO&S_Z2;e;n}}N4QXqq>{e8L?q2O9SZ!D~yfJ2&uUR;)SBD-)2Z;PS8 zN~~%NaURFW8%z(O7f34@BZ(gUpf}%{97~{+3zyoLwTfFiBuE5+w^|ko-L@nb2S@xU zEe^PLPa{J8ZtTd>D?9?9UgVQ7%&K;)lW35n-W_Yop^2q4HdI;0^Cr;mBAw-9rF#2Q zqi8H%O+#2l9wW+3QFJ$VMBi$`eD19RmlRajA&4t{7uVfG@!YetYQ2=3BW47U`w|X( z&>nN}rGp#50DXuiq{>?)4g=C%T#~zwwuLn@HS;$G(xQk0XsItL)ZC diff --git a/vue/public/t_race/index.html b/vue/public/t_race/index.html deleted file mode 100644 index 2670170..0000000 --- a/vue/public/t_race/index.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/vue/public/vite.svg b/vue/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/vue/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/vue/src/App.vue b/vue/src/App.vue deleted file mode 100644 index 58e9b02..0000000 --- a/vue/src/App.vue +++ /dev/null @@ -1,1204 +0,0 @@ - - -