From e22d33b65493b3fd7876966b612d344701645d9b Mon Sep 17 00:00:00 2001 From: voltsrage Date: Sat, 27 Jun 2026 12:15:43 +0800 Subject: [PATCH] feature: Digitization Workstation UI --- README.md | 260 +- .../Controllers/PatientsController.cs | 25 +- .../Controllers/UsersController.cs | 32 + .../Controllers/WorkQueueController.cs | 13 + .../Records/Patient/PatientSearchResult.cs | 6 + .../Records/User/UserSummaryResponse.cs | 7 + .../WorkQueue/WorkQueueOverviewResponse.cs | 10 + VigilCareRecordsAPI/Program.cs | 2 + .../Interfaces/IPatientRegistryService.cs | 4 + .../Interfaces/IUserDirectoryService.cs | 4 + .../Services/Interfaces/IWorkQueueService.cs | 5 + .../Services/PatientRegistryService.cs | 27 + .../Services/UserDirectoryService.cs | 25 + .../Services/WorkQueueService.cs | 77 +- docs/digitization-workstation-guide.md | 205 ++ .../run-vigilcare-records-verification-p9.sh | 477 +++ vigilcare-records-web/.gitignore | 24 + vigilcare-records-web/.vscode/extensions.json | 3 + vigilcare-records-web/README.md | 5 + vigilcare-records-web/index.html | 13 + vigilcare-records-web/package-lock.json | 2716 +++++++++++++++++ vigilcare-records-web/package.json | 29 + vigilcare-records-web/postcss.config.js | 6 + vigilcare-records-web/public/favicon.svg | 1 + vigilcare-records-web/public/icons.svg | 24 + vigilcare-records-web/src/App.vue | 7 + vigilcare-records-web/src/api/client.ts | 173 ++ vigilcare-records-web/src/assets/hero.png | Bin 0 -> 13057 bytes vigilcare-records-web/src/assets/main.css | 44 + vigilcare-records-web/src/assets/vite.svg | 1 + vigilcare-records-web/src/assets/vue.svg | 1 + .../src/components/AppHeader.vue | 27 + .../src/components/AssignClerkDialog.vue | 95 + .../src/components/BatchList.vue | 98 + .../src/components/EntryForm.vue | 273 ++ .../src/components/ObservationRow.vue | 110 + .../src/components/PatientSearch.vue | 71 + .../src/components/ScanViewer.vue | 113 + .../src/components/VerificationForm.vue | 256 ++ .../src/composables/usePresignedUrl.ts | 40 + vigilcare-records-web/src/main.ts | 22 + vigilcare-records-web/src/router/index.ts | 92 + vigilcare-records-web/src/stores/auth.ts | 121 + vigilcare-records-web/src/stores/batches.ts | 212 ++ vigilcare-records-web/src/types/index.ts | 145 + vigilcare-records-web/src/views/EntryView.vue | 85 + .../src/views/IntakeView.vue | 179 ++ vigilcare-records-web/src/views/LoginView.vue | 63 + .../src/views/QueueDashboardView.vue | 141 + .../src/views/VerificationView.vue | 86 + vigilcare-records-web/tailwind.config.js | 34 + vigilcare-records-web/tsconfig.app.json | 14 + vigilcare-records-web/tsconfig.json | 7 + vigilcare-records-web/tsconfig.node.json | 23 + vigilcare-records-web/vite.config.ts | 21 + 55 files changed, 6411 insertions(+), 143 deletions(-) create mode 100644 VigilCareRecordsAPI/Controllers/UsersController.cs create mode 100644 VigilCareRecordsAPI/Models/Records/Patient/PatientSearchResult.cs create mode 100644 VigilCareRecordsAPI/Models/Records/User/UserSummaryResponse.cs create mode 100644 VigilCareRecordsAPI/Models/Records/WorkQueue/WorkQueueOverviewResponse.cs create mode 100644 VigilCareRecordsAPI/Services/Interfaces/IPatientRegistryService.cs create mode 100644 VigilCareRecordsAPI/Services/Interfaces/IUserDirectoryService.cs create mode 100644 VigilCareRecordsAPI/Services/PatientRegistryService.cs create mode 100644 VigilCareRecordsAPI/Services/UserDirectoryService.cs create mode 100644 docs/digitization-workstation-guide.md create mode 100644 scripts/run-vigilcare-records-verification-p9.sh create mode 100644 vigilcare-records-web/.gitignore create mode 100644 vigilcare-records-web/.vscode/extensions.json create mode 100644 vigilcare-records-web/README.md create mode 100644 vigilcare-records-web/index.html create mode 100644 vigilcare-records-web/package-lock.json create mode 100644 vigilcare-records-web/package.json create mode 100644 vigilcare-records-web/postcss.config.js create mode 100644 vigilcare-records-web/public/favicon.svg create mode 100644 vigilcare-records-web/public/icons.svg create mode 100644 vigilcare-records-web/src/App.vue create mode 100644 vigilcare-records-web/src/api/client.ts create mode 100644 vigilcare-records-web/src/assets/hero.png create mode 100644 vigilcare-records-web/src/assets/main.css create mode 100644 vigilcare-records-web/src/assets/vite.svg create mode 100644 vigilcare-records-web/src/assets/vue.svg create mode 100644 vigilcare-records-web/src/components/AppHeader.vue create mode 100644 vigilcare-records-web/src/components/AssignClerkDialog.vue create mode 100644 vigilcare-records-web/src/components/BatchList.vue create mode 100644 vigilcare-records-web/src/components/EntryForm.vue create mode 100644 vigilcare-records-web/src/components/ObservationRow.vue create mode 100644 vigilcare-records-web/src/components/PatientSearch.vue create mode 100644 vigilcare-records-web/src/components/ScanViewer.vue create mode 100644 vigilcare-records-web/src/components/VerificationForm.vue create mode 100644 vigilcare-records-web/src/composables/usePresignedUrl.ts create mode 100644 vigilcare-records-web/src/main.ts create mode 100644 vigilcare-records-web/src/router/index.ts create mode 100644 vigilcare-records-web/src/stores/auth.ts create mode 100644 vigilcare-records-web/src/stores/batches.ts create mode 100644 vigilcare-records-web/src/types/index.ts create mode 100644 vigilcare-records-web/src/views/EntryView.vue create mode 100644 vigilcare-records-web/src/views/IntakeView.vue create mode 100644 vigilcare-records-web/src/views/LoginView.vue create mode 100644 vigilcare-records-web/src/views/QueueDashboardView.vue create mode 100644 vigilcare-records-web/src/views/VerificationView.vue create mode 100644 vigilcare-records-web/tailwind.config.js create mode 100644 vigilcare-records-web/tsconfig.app.json create mode 100644 vigilcare-records-web/tsconfig.json create mode 100644 vigilcare-records-web/tsconfig.node.json create mode 100644 vigilcare-records-web/vite.config.ts diff --git a/README.md b/README.md index 217a30b..a5af5a9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# VigilCare Records API +# VigilCare Records -A clinical records intake backend built with ASP.NET Core 8, PostgreSQL, MinIO, and Redis. The domain models the scan-to-approved lifecycle at the center of any paper chart digitization system: scanned document upload, human data entry, dual-human verification with separation of duties, site-configurable clinical approval routing, atomic promotion to live clinical tables, and governed correction via supersession. +A clinical records digitization platform built with ASP.NET Core 8, PostgreSQL, MinIO, Redis, and a Vue 3 workstation UI. The domain models the scan-to-approved lifecycle at the center of any paper chart digitization system: scanned document upload, human data entry, dual-human verification with separation of duties, site-configurable clinical approval routing, atomic promotion to live clinical tables, and governed correction via supersession. -**Implementation status:** Six planned phases are complete through Phase 6 — from schema, authentication, and batch CRUD through draft data entry, verification/rejection with separation of duties, approval with atomic promotion to VigilCareClinical live tables, correction batches that supersede erroneous promoted observations without silent edits, and Track B live capture with clinician attestation and synchronous critical alerting. See [Implemented Phases](#implemented-phases) for the full breakdown. +**Implementation status:** Phases 1–6 are complete (API core through Track B live capture). Phase 7 delivers the digitization workstation UI (`vigilcare-records-web`). Phase 8 is partially implemented — work queue overview endpoint and supervisor dashboard UI are in place; Prometheus metrics and promotion retry remain planned. See [Implemented Phases](#implemented-phases) for the full breakdown. ## Domain Model — How It Maps to a Real Clinical System @@ -55,7 +55,10 @@ Append-only audit log entry for every state transition, field-level correction, - **Promotion Result Query** — `GET /digitization-batches/:id/promotion-result` returns live entity IDs (patient, MRN, encounter, observations) created during promotion - **Corrections and Supersession** — approved live observations are never silently edited; a correction uploads a new batch with `supersedesBatchId`, goes through the full entry → verify → approve cycle, and on promotion marks the original batch's `live_observations` rows as `is_superseded` (append-only — never deleted); `422 SUPERSEDED_BATCH_NOT_PROMOTED`, `409 BATCH_ALREADY_SUPERSEDED`, and `404 SUPERSEDED_BATCH_NOT_FOUND` guard invalid supersession targets; correction batches inherit patient context and reuse the original promotion encounter - **Patient Digitization History** — `GET /patients/:id/digitization-history` returns all batches for a patient with correction chain metadata (`isCorrection`, `hasBeenSuperseded`, `supersededByBatchId`), live vs superseded observation counts, summary totals, and per-batch audit trails; `404 PATIENT_HISTORY_NOT_FOUND` when no batches exist for the patient -- **Work Queues** — `GET /work-queue/verification` (batches in `PENDING_VERIFICATION`, sorted by submission time ASC); `GET /work-queue/entry` (batches in `UPLOADED`, `IN_ENTRY`, or `REJECTED`); `GET /work-queue/clinical-approval` (batches in `AWAITING_CLINICAL_APPROVAL`); role-restricted access +- **Patient Registry Search** — `GET /patients/search?q=` searches live patients by MRN or full name (minimum 2 characters); used by the intake workstation to link uploads to existing patients +- **Work Queues** — `GET /work-queue/verification` (batches in `PENDING_VERIFICATION`, sorted by submission time ASC); `GET /work-queue/entry` (batches in `UPLOADED`, `IN_ENTRY`, or `REJECTED`); `GET /work-queue/clinical-approval` (batches in `AWAITING_CLINICAL_APPROVAL`); `GET /work-queue/overview` (aggregate status counts, average queue age, reject rate, oldest pending verification — administrator only); role-restricted access +- **User Directory** — `GET /users?role=` lists active users for batch assignment (intake clerks assign entry clerks via the workstation UI) +- **Digitization Workstation UI** — Vue 3 SPA at `vigilcare-records-web`: role-based routing (intake, entry, verification, supervisor dashboard), split-pane scan viewer with zoom/pan/rotate, draft entry with auto-save, field-level verification checkboxes, presigned URL refresh for long sessions, JWT refresh interceptor - **Live Capture (Track B)** — `POST /live-capture/encounters/{encounterId}/observations` and `POST /live-capture/encounters` for credentialed clinicians entering vitals at bedside; clinician attestation + password re-confirm replaces the dual-human verification gate; observations promote synchronously to live tables in a single transaction with `source = live_capture`; critical threshold evaluation runs before the response returns, with inline `criticalAlert` per observation, committed `ClinicalAlert` rows, and `observation.recorded` / `alert.generated` outbox events; `422 ATTESTATION_REQUIRED`, `422 PASSWORD_CONFIRM_INVALID`, `422 EMPTY_OBSERVATIONS`, `409 ENCOUNTER_NOT_ACTIVE`, `409 ACTIVE_ENCOUNTER_EXISTS` - **Batch Status Machine** — explicit transition matrix enforced in the service layer; illegal transitions return `409`; `PROMOTED` is terminal — corrections require a new batch with `supersedesBatchId` - **JWT Authentication** — `POST /auth/login` returns access token (15 min) and refresh token (7 days); `POST /auth/refresh` rotates tokens; `POST /auth/logout` revokes server-side; `GET /auth/me` returns authenticated user profile; BCrypt password hashing @@ -85,7 +88,9 @@ HTTP request ├── DigitizationHistoryService (patient digitization history with correction chain and audit trails) ├── IdempotencyService (Idempotency-Key storage + replay for safe promotion retries) ├── MrnGenerator (PostgreSQL sequence-backed MRN generation: VCR-000001) - ├── WorkQueueService (verification, entry, clinical approval queues) + ├── WorkQueueService (verification, entry, clinical approval queues, supervisor overview metrics) + ├── PatientRegistryService (live patient search by MRN or name) + ├── UserDirectoryService (active user listing for batch assignment) ├── AttestationService (clinician role + password re-confirm for live capture) ├── LiveCaptureService (Track B synchronous promotion + critical alert evaluation) ├── DocumentStorageService (MinIO upload, SHA-256, presigned URLs) @@ -122,6 +127,7 @@ HTTP request | Layer | Technology | |---|---| | Server | ASP.NET Core 8 (.NET 8.0) | +| Frontend | Vue 3, Vite, Pinia, Vue Router, Axios, Tailwind CSS, VueUse | | Database | PostgreSQL 16 with EF Core 8 (code-first migrations) | | Cache / locking | Redis 7 (batch assignment locks, alert threshold cache for live capture) | | Object storage | MinIO (scanned documents — PDF, JPEG, PNG) | @@ -136,134 +142,50 @@ HTTP request ## Project Structure ``` -VigilCareRecordsAPI/ -├── Program.cs # Service registration, middleware, seed on startup -├── appsettings.json # Connection strings, Redis, MinIO, Seq, JWT, site config -├── Controllers/ -│ ├── ApprovalController.cs # Batch approval and promotion to live clinical tables -│ ├── AuthController.cs # JWT login, token refresh, logout, authenticated user profile -│ ├── DigitizationBatchesController.cs # Batch CRUD, document upload, assignment -│ ├── DraftController.cs # Draft data entry: patient, encounter, observations, submit -│ ├── PatientsController.cs # Patient digitization history with correction chain -│ ├── LiveCaptureController.cs # Track B live capture: record vitals, open encounter + vitals -│ ├── VerificationController.cs # Batch verification and rejection with separation of duties -│ └── WorkQueueController.cs # Work queues: verification, entry, clinical approval -├── Domain/ -│ ├── Entities/ -│ │ ├── Clinical/ -│ │ │ ├── Patient.cs # Live patient record with MRN (promoted from draft) -│ │ │ ├── Encounter.cs # Live encounter (promoted from draft) -│ │ │ ├── Observation.cs # Live observation with source traceability (batchId, draftObsId) -│ │ │ ├── OutboxEvent.cs # Transactional outbox for downstream consumers (eventType, aggregateType, payloadJson) -│ │ │ ├── AlertThreshold.cs # Critical/warning bounds per observation code (live capture alerting) -│ │ │ ├── ClinicalAlert.cs # Synchronous critical alerts (AlertType, Severity, Details) -│ │ │ └── IdempotencyRecord.cs # Idempotency-Key storage for safe promotion retries -│ │ ├── Draft/ -│ │ │ ├── DraftPatient.cs # Structured patient demographics from paper chart -│ │ │ ├── DraftEncounter.cs # Encounter context: admission, department, room/bed -│ │ │ └── DraftObservation.cs # Single measurement: code, value, unit, recordedAt -│ │ ├── DigitizationBatch.cs # Central workflow entity with status machine -│ │ ├── LiveEncounter.cs # Live encounter mirror for supersession / history queries -│ │ ├── LiveObservation.cs # Live observation with supersession flags (is_superseded, superseded_by_batch_id) -│ │ ├── ScannedDocument.cs # MinIO object reference with SHA-256 integrity hash -│ │ ├── DigitizationEvent.cs # Append-only audit trail for every status transition -│ │ ├── User.cs # Username, BCrypt hash, full name, role, active flag -│ │ ├── RefreshToken.cs # Opaque refresh token with user FK, expiry, revocation -│ │ └── AuthAuditEvent.cs # Auth event audit: login, logout, refresh, failed attempts -│ └── Enums/ -│ ├── BatchStatus.cs # Uploaded → InEntry → PendingVerification → Verified/AwaitingClinicalApproval → Approved → Promoted -│ ├── BatchType.cs # PatientRegistration, VitalsSheet, LabResults, EncounterSummary, MedicationList, AllergyUpdate, Mixed -│ ├── BatchTrack.cs # Backfill (Track A) or LiveCapture (Track B) -│ ├── UserRole.cs # IntakeClerk, DataEntryClerk, Verifier, ClinicalApprover, Clinician, Administrator -│ ├── DigitizationEventType.cs # 18 event types covering full lifecycle + corrections + promotion retry -│ ├── BloodType.cs # A+, A-, B+, B-, AB+, AB-, O+, O- -│ ├── Department.cs # Clinical departments -│ ├── AlertType.cs # Critical/warning alert types (ported from VigilCareClinical) -│ ├── AlertSeverity.cs # WARNING, CRITICAL -│ └── AlertStatus.cs # OPEN, ACKNOWLEDGED, RESOLVED, ESCALATED -├── Services/ -│ ├── Interfaces/ # IAuthService, IBatchService, IDraftService, IVerificationService, IWorkQueueService, IDocumentStorageService, IPromotionService, IIdempotencyService, IMrnGenerator, IDigitizationHistoryService, ILiveCaptureService, IAttestationService -│ ├── AuthService.cs # Login (BCrypt verify), JWT generation, refresh rotation, logout, audit logging -│ ├── BatchService.cs # Batch CRUD, status machine transitions, Redis assignment lock, duplicate detection, supersession validation on create -│ ├── DraftService.cs # Draft CRUD with plausibility validation, batch-type completeness on submit (relaxed for correction batches) -│ ├── VerificationService.cs # Verify/reject with separation of duties, site-config clinical approval routing -│ ├── PromotionService.cs # Atomic approve + promote: draft → live tables in single transaction; supersede original observations on correction promotion -│ ├── DigitizationHistoryService.cs # Patient history with correction chain, observation counts, audit trails -│ ├── IdempotencyService.cs # Idempotency-Key record storage + replay (24h TTL) -│ ├── MrnGenerator.cs # PostgreSQL sequence-backed MRN generation (VCR-000001) -│ ├── WorkQueueService.cs # Verification, entry, and clinical approval work queues -│ ├── DocumentStorageService.cs # MinIO upload with SHA-256, presigned URL generation -│ ├── AttestationService.cs # Clinician role + password re-confirm for live capture -│ ├── Interfaces/LiveCaptureService.cs # Track B synchronous promotion + critical alert evaluation -│ └── PlausibilityValidator.cs # Per-code numeric range guard (reused from VigilCareClinical) -├── Configurations/ -│ ├── JwtOptions.cs # Issuer, audience, signing key, access/refresh token expiration -│ ├── MinioOptions.cs # Endpoint, credentials, bucket, presigned URL expiry -│ └── SiteConfigOptions.cs # ClinicalApprovalRequired map per batch type -├── Models/Records/ -│ ├── Auth/ # LoginRequest, LoginResponse, RefreshRequest, LogoutRequest, TokenResponse, UserProfileResponse -│ ├── Batch/ # CreateBatchForm, CreateBatchRequest, AssignBatchRequest, BatchDetailResponse, SupersessionInfo, PatientDigitizationHistoryResponse, DigitizationHistoryEntry, ... -│ ├── Promotion/ # ApproveRequest, PromotionResultResponse -│ ├── DraftPatient/ # DraftPatientDto, UpsertDraftPatientRequest -│ ├── DraftEncounter/ # DraftEncounterDto, UpsertDraftEncounterRequest -│ ├── DraftObservation/ # DraftObservationDto, CreateDraftObservationRequest, UpdateDraftObservationRequest -│ ├── WorkQueue/ # WorkQueueResponse, WorkQueueItemResponse -│ ├── LiveCapture/ # LiveCaptureResponse, RecordObservationsRequest, ThresholdCacheEntry, ... -│ └── Common/ # PagedResult -├── Data/ -│ ├── AppDbContext.cs # EF Core context — entity configs, indexes, constraints -│ ├── Configurations/ -│ │ ├── Clinical/ # PatientConfiguration, EncounterConfiguration, ObservationConfiguration, OutboxEventConfiguration, AlertThresholdConfiguration, ClinicalAlertConfiguration, IdempotencyRecordConfiguration -│ │ ├── Draft/ # DraftPatientConfiguration, DraftEncounterConfiguration, DraftObservationConfiguration -│ │ ├── LiveEncounterConfiguration.cs # live_encounters table mapping -│ │ ├── LiveObservationConfiguration.cs # live_observations with supersession columns and partial index -│ │ └── ... # DigitizationBatch, DigitizationEvent, ScannedDocument, User, RefreshToken, AuthAuditEvent configs -│ ├── Seed/ -│ │ └── DataSeeder.cs # Seeds 12 demo users (two per role) on startup -│ └── Migrations/ # InitialCreate through AddAlertThresholdsAndClinicalAlerts -├── Common/ -│ ├── ApiResponse.cs # { success, statusCode, data, error } envelope -│ └── Exceptions/ -│ ├── NotFoundException.cs -│ ├── ConflictException.cs # Status machine, separation of duties, assignment conflicts -│ ├── BadRequestException.cs -│ ├── DomainException.cs -│ └── ValidationException.cs -├── Middleware/ -│ ├── CorrelationIdMiddleware.cs # Per-request correlation IDs -│ └── ExceptionHandlerMiddleware.cs # Consistent error responses -└── Infrastructure/ - └── OpenApi/ - └── SwaggerServiceCollectionExtensions.cs - -tests/ -└── VigilCareRecordsAPI.Tests/ - ├── DraftEntryTests.cs # Draft CRUD, plausibility validation, submit-for-verification completeness - ├── VerificationTests.cs # Verification, rejection, separation of duties, clinical approval routing - ├── PromotionTests.cs # Approval, atomic promotion, idempotency, separation of duties, retroactive alerts, patient dedup - ├── CorrectionSupersessionTests.cs # Correction batch supersession, validation guards, patient digitization history - ├── LiveCaptureIntegrationTests.cs # Track B attestation, synchronous promotion, critical alerts, open encounter workflow - ├── Fixtures/ - │ ├── ApiFixture.cs # WebApplicationFactory with PostgreSQL, Redis, MinIO containers - │ └── DatabaseCollection.cs # Shared test collection - └── Helpers/ - ├── AuthHelper.cs # JWT token generation for test users - ├── BatchSeedHelper.cs # Creates seeded batches at various lifecycle stages - ├── BatchPipelineHelper.cs # End-to-end batch pipeline: upload → entry → verify → ready for approval - ├── CorrectionPipelineHelper.cs # Lab correction pipeline: promote original → promote correction batch - └── DbResetHelper.cs # Database cleanup between tests - -scripts/ -├── run-vigilcare-records-verification.sh # Phase 1 — schema, auth, roles, batch CRUD, MinIO, status machine -├── run-vigilcare-records-phase-2-verification.sh # Phase 2 — draft entry, plausibility, submit-for-verification -├── run-vigilcare-records-phase-3-verification.sh # Phase 3 — verification, rejection, separation of duties, work queues -├── run-vigilcare-records-phase-4-verification.sh # Phase 4 — approval, atomic promotion, idempotency, patient dedup, outbox events -├── run-vigilcare-records-phase-5-verification.sh # Phase 5 — corrections, supersession, digitization history, audit trail -└── run-vigilcare-records-phase-6-verification.sh # Phase 6 — live capture, attestation, synchronous alerts, outbox events - -docs/ -├── plans/ # Phase 1–9 implementation and verification guides -└── vigilcare-records-prd.md # Product requirements and phase roadmap +VigilCareRecords/ +├── VigilCareRecordsAPI/ +│ ├── Program.cs # Service registration, middleware, seed on startup +│ ├── appsettings.json # Connection strings, Redis, MinIO, Seq, JWT, site config +│ ├── Controllers/ +│ │ ├── ApprovalController.cs # Batch approval and promotion to live clinical tables +│ │ ├── AuthController.cs # JWT login, token refresh, logout, authenticated user profile +│ │ ├── DigitizationBatchesController.cs # Batch CRUD, document upload, assignment +│ │ ├── DraftController.cs # Draft data entry: patient, encounter, observations, submit +│ │ ├── PatientsController.cs # Patient search and digitization history +│ │ ├── UsersController.cs # User directory for batch assignment +│ │ ├── LiveCaptureController.cs # Track B live capture: record vitals, open encounter + vitals +│ │ ├── VerificationController.cs # Batch verification and rejection with separation of duties +│ │ └── WorkQueueController.cs # Work queues and supervisor overview +│ ├── Domain/ … # Entities, enums (batch, draft, clinical, user) +│ ├── Services/ … # Auth, batch, draft, verification, promotion, work queue, patient registry, user directory, live capture +│ ├── Models/Records/ … # Request/response DTOs +│ ├── Data/ … # EF Core context, configurations, migrations, seed +│ └── … # Middleware, Common, Infrastructure +├── vigilcare-records-web/ # Vue 3 digitization workstation UI +│ ├── src/ +│ │ ├── api/client.ts # Axios client with JWT interceptor and proactive refresh +│ │ ├── stores/ # Pinia: auth (login, roles, routing), batches (CRUD, draft, verify) +│ │ ├── router/index.ts # Role-based routes and navigation guards +│ │ ├── views/ # Login, Intake, Entry, Verification, QueueDashboard +│ │ ├── components/ # ScanViewer, EntryForm, VerificationForm, BatchList, PatientSearch, AppHeader, AssignClerkDialog +│ │ ├── composables/usePresignedUrl.ts # Refreshes presigned document URLs before 15-minute expiry +│ │ └── types/index.ts # TypeScript interfaces matching API response shapes +│ ├── vite.config.ts # Dev server on port 3028; proxies /api → localhost:5217 +│ └── tailwind.config.js # Clinical color palette and layout component classes +├── tests/ +│ └── VigilCareRecordsAPI.Tests/ # Integration tests (Phases 1–6) +├── scripts/ +│ ├── run-vigilcare-records-verification.sh # Phase 1 +│ ├── run-vigilcare-records-phase-2-verification.sh +│ ├── run-vigilcare-records-phase-3-verification.sh +│ ├── run-vigilcare-records-phase-4-verification.sh +│ ├── run-vigilcare-records-phase-5-verification.sh +│ ├── run-vigilcare-records-phase-6-verification.sh +│ └── run-vigilcare-records-verification-p9.sh # E2E workflow + work queue overview smoke test +└── docs/ + ├── plans/ # Phase 1–9 implementation guides + ├── digitization-workstation-guide.md # Clerk workflow and UI reference + └── vigilcare-records-prd.md # Product requirements and phase roadmap ``` --- @@ -378,6 +300,7 @@ Approved live observations are never mutated or deleted. When a transcription er ### Prerequisites - .NET 8 SDK +- Node.js 20+ and npm (for the workstation UI) - Docker and Docker Compose ### Start Infrastructure @@ -407,6 +330,34 @@ On startup the application: Swagger UI is available at `http://localhost:5217/swagger` in Development. +### Run the Workstation UI + +With the API running: + +```bash +cd vigilcare-records-web +npm install +npm run dev +``` + +Open `http://localhost:3028`. The Vite dev server proxies `/api` requests to the API on port 5217. + +| Username | Password | Default route | +|---|---|---| +| `intake1` | `password` | `/intake` — upload scans, assign entry clerks | +| `entry1` | `password` | `/entry` — data entry queue and split-pane form | +| `verifier1` | `password` | `/verification` — field-level verification | +| `admin1` | `password` | `/dashboard` — supervisor queue overview | + +See [docs/digitization-workstation-guide.md](docs/digitization-workstation-guide.md) for the full clerk workflow. + +Production build: + +```bash +cd vigilcare-records-web +npm run build # output in dist/ +``` + ### Run Tests ```bash @@ -434,6 +385,7 @@ With the API running (`dotnet run`) and Docker Compose up: ./scripts/run-vigilcare-records-phase-4-verification.sh # Phase 4 — approval, promotion, idempotency, patient dedup ./scripts/run-vigilcare-records-phase-5-verification.sh # Phase 5 — corrections, supersession, digitization history ./scripts/run-vigilcare-records-phase-6-verification.sh # Phase 6 — live capture, attestation, critical alerts +./scripts/run-vigilcare-records-verification-p9.sh # E2E workflow + work queue overview smoke test ``` --- @@ -657,8 +609,40 @@ Error response: | GET | `/work-queue/verification` | Verifier, Clinical Approver, Administrator | Batches in `PENDING_VERIFICATION`, sorted by submission time | | GET | `/work-queue/entry` | Data Entry Clerk, Administrator | Batches awaiting or in entry (`UPLOADED`, `IN_ENTRY`, `REJECTED`) | | GET | `/work-queue/clinical-approval` | Clinical Approver, Administrator | Batches in `AWAITING_CLINICAL_APPROVAL` | +| GET | `/work-queue/overview` | Administrator | Aggregate metrics: status counts, average queue age, 24h reject rate, oldest pending verification | -All work queue endpoints support pagination via `?page=1&pageSize=20`. +All work queue endpoints support pagination via `?page=1&pageSize=20` (except `overview`). + +**GET `/work-queue/overview` response:** + +| Field | Type | Description | +|---|---|---| +| `statusCounts` | object | Batch count per status (all 8 statuses present) | +| `averageTimeInQueueMinutes` | number | Average age of batches in `PENDING_VERIFICATION` | +| `rejectRate` | number | Rejections / (rejections + verifications) over the last 24 hours (0.0–1.0) | +| `oldestPendingVerificationMinutes` | number | Age of the oldest batch in `PENDING_VERIFICATION` | + +### Patient Registry + +| Method | Path | Auth | Description | +|---|---|---|---| +| GET | `/patients/search?q=` | Intake Clerk, Administrator | Search live patients by MRN or full name (min 2 characters); returns up to 20 matches | + +**Search result object:** + +| Field | Type | Description | +|---|---|---| +| `id` | Guid | Patient ID | +| `fullName` | string | Patient full name | +| `mrn` | string | Medical record number | + +### User Directory + +| Method | Path | Auth | Description | +|---|---|---|---| +| GET | `/users?role=` | Intake Clerk, Administrator | List active users; optional `role` filter (e.g. `DATA_ENTRY_CLERK`) | + +Used by the intake workstation assign-clerk dialog. ### Patient Digitization History @@ -1034,7 +1018,7 @@ Response shape: ## Implemented Phases -Six phases from the project roadmap are implemented and verified. Integration tests (`dotnet test`) and per-phase verification scripts cover Phases 1–6. +Phases 1–6 are fully implemented and verified via integration tests and per-phase scripts. Phase 7 (workstation UI) and parts of Phase 8 (supervisor overview) are implemented. | Phase | Feature | Status | |---|---|---| @@ -1044,6 +1028,6 @@ Six phases from the project roadmap are implemented and verified. Integration te | 4 | Approval with three-person separation of duties (entry ≠ verifier ≠ approver), atomic promotion to live `patients`/`encounters`/`observations` tables in a single PostgreSQL transaction, PostgreSQL sequence-backed MRN generation (`VCR-NNNNNN`), patient dedup by name + DOB, encounter matching by patient + department + active status, transactional outbox (`observation.created` events), retroactive alert policy per batch, `Idempotency-Key` header with 24h TTL for safe retries, `PromotionTests` integration tests, Phase 4 verification script | Done | | 5 | Correction batches via `supersedesBatchId`, supersession validation on create (`422`/`404`/`409`), append-only `live_observations` supersession flags (`is_superseded`, `superseded_by_batch_id`, `superseded_at`), correction promotion reuses original encounter and linked patient, `correction_uploaded`/`correction_promoted`/`superseded` audit events, `GET /patients/:id/digitization-history` with correction chain and per-batch audit trails, `CorrectionSupersessionTests` integration tests, Phase 5 verification script | Done | | 6 | Track B live capture: `LiveCaptureController` with clinician-only endpoints, `AttestationService` (role + password re-confirm), synchronous promotion via `LiveCaptureService`, Redis-backed critical threshold evaluation, inline critical alerts + committed `ClinicalAlert` rows, `observation.recorded` and `alert.generated` outbox events, open-encounter + vitals outpatient workflow, `LiveCaptureIntegrationTests`, Phase 6 verification script | Done | -| 7 | Digitization workstation UI (Vue 3 side-by-side scan viewer + entry form) | Planned | -| 8 | Prometheus metrics, supervisor dashboard, promotion retry job | Planned | -| 9 | Seed data, E2E verification script, clinical scenario documentation | Planned | +| 7 | Digitization workstation UI (`vigilcare-records-web`): Vue 3 + Pinia + Tailwind, role-based routing, split-pane scan viewer, draft entry with auto-save, verification checkboxes, patient search, assign-clerk dialog, presigned URL refresh, supervisor dashboard shell | Done | +| 8 | `GET /work-queue/overview`, supervisor dashboard UI, patient search API, user directory API | Partial — Prometheus metrics and promotion retry job planned | +| 9 | E2E verification script (`run-vigilcare-records-verification-p9.sh`), clinical scenario documentation | Partial | diff --git a/VigilCareRecordsAPI/Controllers/PatientsController.cs b/VigilCareRecordsAPI/Controllers/PatientsController.cs index 8a007c1..1533a9f 100644 --- a/VigilCareRecordsAPI/Controllers/PatientsController.cs +++ b/VigilCareRecordsAPI/Controllers/PatientsController.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; - /// -/// Patient-scoped endpoints for digitization history and audit trail. +/// Patient registry search and digitization history. /// [ApiController] [Route("api/v1/patients")] @@ -12,9 +11,27 @@ using Microsoft.AspNetCore.Mvc; public class PatientsController : ControllerBase { private readonly IDigitizationHistoryService _history; + private readonly IPatientRegistryService _patients; - public PatientsController(IDigitizationHistoryService history) => + public PatientsController( + IDigitizationHistoryService history, + IPatientRegistryService patients) + { _history = history; + _patients = patients; + } + + /// + /// Searches live patients by MRN or full name (minimum 2 characters). + /// + [HttpGet("search")] + [Authorize(Roles = "INTAKE_CLERK,ADMINISTRATOR")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task Search([FromQuery] string q) + { + var results = await _patients.SearchAsync(q ?? string.Empty); + return Ok(ApiResponse>.Ok(results)); + } /// /// Returns the complete digitization history for a patient, including all @@ -29,4 +46,4 @@ public class PatientsController : ControllerBase var history = await _history.GetPatientHistoryAsync(patientId); return Ok(ApiResponse.Ok(history)); } -} \ No newline at end of file +} diff --git a/VigilCareRecordsAPI/Controllers/UsersController.cs b/VigilCareRecordsAPI/Controllers/UsersController.cs new file mode 100644 index 0000000..40c6c20 --- /dev/null +++ b/VigilCareRecordsAPI/Controllers/UsersController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +/// +/// User directory for batch assignment and operational lookups. +/// +[ApiController] +[Route("api/v1/users")] +[Produces("application/json")] +[Authorize] +public class UsersController : ControllerBase +{ + private readonly IUserDirectoryService _users; + + public UsersController(IUserDirectoryService users) => _users = users; + + /// + /// Lists active users, optionally filtered by role. + /// + [HttpGet] + [Authorize(Roles = "INTAKE_CLERK,ADMINISTRATOR")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task List([FromQuery] string? role) + { + UserRole? parsedRole = null; + if (!string.IsNullOrWhiteSpace(role)) + parsedRole = UserRoleExtensions.FromDbString(role); + + var results = await _users.ListByRoleAsync(parsedRole); + return Ok(ApiResponse>.Ok(results)); + } +} diff --git a/VigilCareRecordsAPI/Controllers/WorkQueueController.cs b/VigilCareRecordsAPI/Controllers/WorkQueueController.cs index e0c315e..dc7365e 100644 --- a/VigilCareRecordsAPI/Controllers/WorkQueueController.cs +++ b/VigilCareRecordsAPI/Controllers/WorkQueueController.cs @@ -68,4 +68,17 @@ public class WorkQueueController : ControllerBase var result = await _workQueue.GetClinicalApprovalQueueAsync(page, pageSize); return Ok(ApiResponse.Ok(result)); } + + /// + /// Returns aggregate work queue health metrics for the supervisor dashboard. + /// + [HttpGet("overview")] + [Authorize(Roles = "ADMINISTRATOR")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] + public async Task GetOverview() + { + var overview = await _workQueue.GetOverviewAsync(); + return Ok(ApiResponse.Ok(overview)); + } } \ No newline at end of file diff --git a/VigilCareRecordsAPI/Models/Records/Patient/PatientSearchResult.cs b/VigilCareRecordsAPI/Models/Records/Patient/PatientSearchResult.cs new file mode 100644 index 0000000..c0077fa --- /dev/null +++ b/VigilCareRecordsAPI/Models/Records/Patient/PatientSearchResult.cs @@ -0,0 +1,6 @@ +/// Patient match returned by GET /api/v1/patients/search. +public record PatientSearchResult( + Guid Id, + string FullName, + string Mrn +); diff --git a/VigilCareRecordsAPI/Models/Records/User/UserSummaryResponse.cs b/VigilCareRecordsAPI/Models/Records/User/UserSummaryResponse.cs new file mode 100644 index 0000000..3a6599c --- /dev/null +++ b/VigilCareRecordsAPI/Models/Records/User/UserSummaryResponse.cs @@ -0,0 +1,7 @@ +/// User summary for assignment and directory lookups. +public record UserSummaryResponse( + Guid Id, + string Username, + string FullName, + string Role +); diff --git a/VigilCareRecordsAPI/Models/Records/WorkQueue/WorkQueueOverviewResponse.cs b/VigilCareRecordsAPI/Models/Records/WorkQueue/WorkQueueOverviewResponse.cs new file mode 100644 index 0000000..c15242f --- /dev/null +++ b/VigilCareRecordsAPI/Models/Records/WorkQueue/WorkQueueOverviewResponse.cs @@ -0,0 +1,10 @@ +/// +/// Aggregate work queue health metrics for the supervisor dashboard. +/// Returned by GET /api/v1/work-queue/overview. +/// +public record WorkQueueOverviewResponse( + Dictionary StatusCounts, + double AverageTimeInQueueMinutes, + double RejectRate, + double OldestPendingVerificationMinutes +); diff --git a/VigilCareRecordsAPI/Program.cs b/VigilCareRecordsAPI/Program.cs index cbca9aa..46bfebf 100644 --- a/VigilCareRecordsAPI/Program.cs +++ b/VigilCareRecordsAPI/Program.cs @@ -62,6 +62,8 @@ try builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/VigilCareRecordsAPI/Services/Interfaces/IPatientRegistryService.cs b/VigilCareRecordsAPI/Services/Interfaces/IPatientRegistryService.cs new file mode 100644 index 0000000..6dda53a --- /dev/null +++ b/VigilCareRecordsAPI/Services/Interfaces/IPatientRegistryService.cs @@ -0,0 +1,4 @@ +public interface IPatientRegistryService +{ + Task> SearchAsync(string query, int limit = 20); +} diff --git a/VigilCareRecordsAPI/Services/Interfaces/IUserDirectoryService.cs b/VigilCareRecordsAPI/Services/Interfaces/IUserDirectoryService.cs new file mode 100644 index 0000000..3c970f6 --- /dev/null +++ b/VigilCareRecordsAPI/Services/Interfaces/IUserDirectoryService.cs @@ -0,0 +1,4 @@ +public interface IUserDirectoryService +{ + Task> ListByRoleAsync(UserRole? role); +} diff --git a/VigilCareRecordsAPI/Services/Interfaces/IWorkQueueService.cs b/VigilCareRecordsAPI/Services/Interfaces/IWorkQueueService.cs index b160048..aae31a0 100644 --- a/VigilCareRecordsAPI/Services/Interfaces/IWorkQueueService.cs +++ b/VigilCareRecordsAPI/Services/Interfaces/IWorkQueueService.cs @@ -24,4 +24,9 @@ public interface IWorkQueueService /// This is the clinical approver's work queue. /// Task GetClinicalApprovalQueueAsync(int page, int pageSize); + + /// + /// Returns aggregate work queue health metrics for the supervisor dashboard. + /// + Task GetOverviewAsync(); } \ No newline at end of file diff --git a/VigilCareRecordsAPI/Services/PatientRegistryService.cs b/VigilCareRecordsAPI/Services/PatientRegistryService.cs new file mode 100644 index 0000000..efdfa19 --- /dev/null +++ b/VigilCareRecordsAPI/Services/PatientRegistryService.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; + +public class PatientRegistryService : IPatientRegistryService +{ + private readonly AppDbContext _db; + + public PatientRegistryService(AppDbContext db) => _db = db; + + public async Task> SearchAsync(string query, int limit = 20) + { + var trimmed = query.Trim(); + if (trimmed.Length < 2) + return Array.Empty(); + + var pattern = $"%{trimmed}%"; + + return await _db.Patients + .AsNoTracking() + .Where(p => + EF.Functions.ILike(p.FullName, pattern) || + EF.Functions.ILike(p.Mrn, pattern)) + .OrderBy(p => p.FullName) + .Take(limit) + .Select(p => new PatientSearchResult(p.Id, p.FullName, p.Mrn)) + .ToListAsync(); + } +} diff --git a/VigilCareRecordsAPI/Services/UserDirectoryService.cs b/VigilCareRecordsAPI/Services/UserDirectoryService.cs new file mode 100644 index 0000000..2cf51be --- /dev/null +++ b/VigilCareRecordsAPI/Services/UserDirectoryService.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; + +public class UserDirectoryService : IUserDirectoryService +{ + private readonly AppDbContext _db; + + public UserDirectoryService(AppDbContext db) => _db = db; + + public async Task> ListByRoleAsync(UserRole? role) + { + var query = _db.Users.AsNoTracking().Where(u => u.IsActive); + + if (role.HasValue) + query = query.Where(u => u.Role == role.Value); + + return await query + .OrderBy(u => u.FullName) + .Select(u => new UserSummaryResponse( + u.Id, + u.Username, + u.FullName, + u.Role.ToDbString())) + .ToListAsync(); + } +} diff --git a/VigilCareRecordsAPI/Services/WorkQueueService.cs b/VigilCareRecordsAPI/Services/WorkQueueService.cs index fe0ef4d..f021062 100644 --- a/VigilCareRecordsAPI/Services/WorkQueueService.cs +++ b/VigilCareRecordsAPI/Services/WorkQueueService.cs @@ -9,10 +9,12 @@ using Microsoft.EntityFrameworkCore; public class WorkQueueService : IWorkQueueService { private readonly AppDbContext _db; + private readonly ILogger _logger; - public WorkQueueService(AppDbContext db) + public WorkQueueService(AppDbContext db, ILogger logger) { _db = db; + _logger = logger; } public async Task GetVerificationQueueAsync(int page, int pageSize) @@ -80,4 +82,77 @@ public class WorkQueueService : IWorkQueueService return new WorkQueueResponse(queueName, items, page, pageSize, totalCount, totalPages); } + + public async Task GetOverviewAsync() + { + var now = DateTimeOffset.UtcNow; + + var statusGroups = await _db.DigitizationBatches + .AsNoTracking() + .GroupBy(b => b.Status) + .Select(g => new { Status = g.Key, Count = g.Count() }) + .ToListAsync(); + + var statusCounts = new Dictionary(); + foreach (var status in Enum.GetValues()) + { + var count = statusGroups.FirstOrDefault(g => g.Status == status)?.Count ?? 0; + statusCounts[status.ToDbString()] = count; + } + + var pendingBatches = await _db.DigitizationBatches + .AsNoTracking() + .Where(b => b.Status == BatchStatus.PendingVerification) + .Select(b => b.UpdatedAt) + .ToListAsync(); + + double avgTimeInQueueMinutes = 0; + double oldestPendingMinutes = 0; + + if (pendingBatches.Count > 0) + { + var ages = pendingBatches + .Select(updatedAt => (now - updatedAt).TotalMinutes) + .ToList(); + + avgTimeInQueueMinutes = ages.Average(); + oldestPendingMinutes = ages.Max(); + } + + var cutoff = now.AddHours(-24); + + var recentEvents = await _db.DigitizationEvents + .AsNoTracking() + .Where(e => e.OccurredAt >= cutoff) + .Where(e => e.EventType == DigitizationEventType.Rejected + || e.EventType == DigitizationEventType.Verified + || e.EventType == DigitizationEventType.VerifiedPendingClinical) + .GroupBy(e => e.EventType) + .Select(g => new { EventType = g.Key, Count = g.Count() }) + .ToListAsync(); + + var rejections = recentEvents + .Where(e => e.EventType == DigitizationEventType.Rejected) + .Sum(e => e.Count); + + var verifications = recentEvents + .Where(e => e.EventType == DigitizationEventType.Verified + || e.EventType == DigitizationEventType.VerifiedPendingClinical) + .Sum(e => e.Count); + + var totalDecisions = rejections + verifications; + var rejectRate = totalDecisions > 0 + ? (double)rejections / totalDecisions + : 0.0; + + _logger.LogDebug( + "Work queue overview: {PendingCount} pending, avg queue {AvgMinutes:F1}m, reject rate {RejectRate:P1}", + pendingBatches.Count, avgTimeInQueueMinutes, rejectRate); + + return new WorkQueueOverviewResponse( + StatusCounts: statusCounts, + AverageTimeInQueueMinutes: Math.Round(avgTimeInQueueMinutes, 1), + RejectRate: Math.Round(rejectRate, 4), + OldestPendingVerificationMinutes: Math.Round(oldestPendingMinutes, 1)); + } } \ No newline at end of file diff --git a/docs/digitization-workstation-guide.md b/docs/digitization-workstation-guide.md new file mode 100644 index 0000000..27053b2 --- /dev/null +++ b/docs/digitization-workstation-guide.md @@ -0,0 +1,205 @@ +```markdown +# VigilCare Records — Digitization Workstation Guide + +## Overview + +This guide describes three primary clinical workflows supported by the VigilCare +Records digitization platform: historical backfill, live bedside capture, and +corrections. Each workflow maps to a real operational scenario in a paper-based +hospital or clinic. + +--- + +## Workflow 1: Historical Backfill (Track A) + +**Scenario:** District General Hospital has 200 active patients with paper charts. +The facility is deploying VigilCareClinical for real-time alerting. Before alerts +can fire, historical vital signs and lab results must be digitized into the system. + +**Actors:** Intake Clerk, Data Entry Clerk, Verifier, Clinical Approver (for +high-stakes batch types) + +**Steps:** + +### 1. Intake (Intake Clerk) + +The intake clerk receives a stack of patient charts from the ward. For each chart +section: + +1. Scan the page(s) using a flatbed scanner or document camera +2. Log in to the workstation as `intake1` +3. Navigate to **Intake** view +4. Upload the scan (PDF, JPEG, or PNG; max 25 MB) +5. Select the batch type: + - **Vitals Sheet** — vital signs from nursing observation charts + - **Lab Results** — laboratory test result reports + - **Patient Registration** — face sheet with demographics + - **Encounter Summary** — admission/discharge summaries + - **Allergy Update** — allergy documentation + - **Medication List** — current medication records +6. Set track to **Backfill** (default) +7. Optionally link to an existing patient by searching MRN or name +8. Click **Upload and Create Batch** + +The batch is created in `uploaded` status. The SHA-256 hash prevents duplicate +uploads of the same document for the same patient within 24 hours. + +### 2. Data Entry (Data Entry Clerk) + +The entry clerk opens the batch from their **Entry** queue: + +1. Log in as `entry1` +2. Click the batch to open the split-pane workstation +3. **Left pane:** The scanned document is displayed with zoom, pan, and rotate + controls. The clerk reads the handwritten or printed values from the scan. +4. **Right pane:** Structured form fields for: + - Patient demographics (name, DOB, sex, blood type, emergency contact) + - Encounter context (admission date, department, room/bed, admission reason) + - Observations (one row per vital sign or lab result) + +**Observation entry rules:** +- Each observation requires a code (e.g., `HEART_RATE`), numeric value, unit, and + `recordedAt` timestamp taken from the chart (not scan time) +- Plausibility validation fires on save: + - Heart rate: 20-300 bpm + - Temperature: 25-45 C + - SpO2: 0-100 % + - Potassium: 1.5-10.0 mEq/L + - Glucose: 20-800 mg/dL +- Out-of-range values return `422 OBSERVATION_OUT_OF_PLAUSIBLE_RANGE` +- This catches the most common digitization error: decimal misplacement (5.2 vs 52) + +5. Click **Submit for Verification** when all fields are complete +6. The batch transitions to `pending_verification` + +### 3. Verification (Verifier) + +The verifier reviews the entry against the original scan: + +1. Log in as `verifier1` (must be a different person than the entry clerk) +2. Open the batch from the **Verification** queue +3. **Left pane:** Same scan viewer +4. **Right pane:** Each field has a verification checkbox + - Compare each entered value against the scan + - Check the checkbox when the value matches + - Progress bar shows completion percentage +5. If all fields match: click **Approve - Verified** +6. If any field is wrong: click **Reject** with a mandatory reason + +**Separation of duties:** The system enforces that `enteredByUserId !== verifiedByUserId`. +If the entry clerk tries to verify their own batch, the API returns `409 +SEPARATION_OF_DUTIES_VIOLATION`. + +**On rejection:** The batch returns to `rejected` status. The entry clerk sees the +rejection reason and can correct the draft, then resubmit for verification. + +### 4. Approval and Promotion + +Track A has **two human gates** before live records exist: + +1. **Verifier** — scan comparison (`verify` / `reject` on `pending_verification`) +2. **Clinical approver** — promotion authorization (`approve` on `verified` or `awaiting_clinical_approval`) + +After verification passes, site configuration routes the batch: + +| batchType | Default: next status after verify | +|---|---| +| `patient_registration`, `allergy_update` | `verified` → clinical approver calls `approve` | +| `encounter_summary`, `vitals_sheet`, `lab_results`, `medication_list`, `mixed` | `awaiting_clinical_approval` → clinical approver reviews in **Clinical Approval** queue, then calls `approve` | + +Log in as `approver1` for the approval step. Verifiers (`verifier1`) never call `approve`. + +On approval: +1. The promotion service runs atomically: + - Create or update Patient in VigilCareClinical + - Create or match Encounter + - Insert each DraftObservation as a live Observation + - Write outbox events (alerting suppressed for backfill unless + `enableRetroactiveAlerts: true`) +2. Batch status transitions to `promoted` +3. All observations are now visible in VigilCareClinical's ward dashboard + +**Alert suppression for backfill:** By default, backfilled observations do not +trigger real-time alerts. A historical potassium of 6.2 mEq/L from three days ago +should not page the on-call physician today. The facility can override this per +batch by setting `enableRetroactiveAlerts: true`. + +--- + +## Workflow 2: Live Bedside Capture (Track B) + +**Scenario:** A nurse or physician enters vital signs at the bedside using a tablet. +The observation needs to reach VigilCareClinical's alert pipeline immediately. + +**Actor:** Clinician + +**Steps:** + +1. Log in as `clinician1` +2. Navigate to the live capture endpoint +3. Select or create the encounter +4. Enter observation values with `clinicianAttestation: true` +5. Confirm with password re-entry or PIN + +**What happens:** +- A `DigitizationBatch` is created with `track: live_capture` and lands in `promoted` immediately (workflow states skipped, audit unit retained) +- `DraftObservation` rows and `DigitizationEvent` entries (`live_capture_attested`, `promoted`) are written in the same transaction as live observations +- No verification queue — the clinician's attestation replaces verify + approve for that batch only +- The batch is promoted synchronously +- If a critical value is entered (e.g., potassium 6.8 mEq/L), the synchronous + alert fires before the response returns +- The observation appears in VigilCareClinical's ward dashboard immediately + +**When to use Track B vs Track A:** +- Track B: Current patient encounter, values just measured, clinician is at bedside +- Track A: Historical charts, bulk digitization, values from past encounters + +--- + +## Workflow 3: Corrections + +**Scenario:** After promotion, a reviewer discovers that the SpO2 value was entered +as 94% but the chart actually shows 95%. The promoted observation must be corrected. + +**Rule:** Approved records are never silently edited. Corrections go through the +full pipeline. + +**Steps:** + +1. Create a new batch with `supersedesBatchId` pointing to the original batch +2. Re-enter the corrected values +3. Submit for verification (new entry, new verifier review) +4. On approval and promotion: + - The corrected observations are inserted as new live records + - The original observations are marked `superseded` (soft flag, not deleted) + - The audit trail shows: original values, correction request, new values, + who made each change and when + +**Why not just edit the original?** Clinical audit integrity. A regulator must be +able to see what was originally entered, when it was corrected, and by whom. Silent +edits destroy this chain. + +--- + +## Staffing Considerations + +- In a small facility, one person may serve as both intake clerk and data entry clerk +- The system **never** allows one person to both enter and verify the same batch, + even if they hold both roles +- Minimum staff for full workflow: 2 people (one enters, one verifies) +- The system tracks clerk throughput via the work queue overview endpoint and + Prometheus metrics + +--- + +## Common Issues and Resolution + +| Issue | Cause | Resolution | +|---|---|---| +| `409 DUPLICATE_DOCUMENT` | Same PDF uploaded for same patient within 24h | Check if batch already exists; use a different scan if needed | +| `409 SEPARATION_OF_DUTIES_VIOLATION` | Entry clerk trying to verify own batch | Assign to a different verifier | +| `422 OBSERVATION_OUT_OF_PLAUSIBLE_RANGE` | Value outside allowed range | Check for decimal misplacement (52 vs 5.2) | +| `409 ILLEGAL_STATUS_TRANSITION` | Trying to skip a workflow step | Follow the status machine: uploaded -> in_entry -> pending_verification -> verified -> approved -> promoted | +| High rejection rate (>15%) | Scan quality or training issues | Review rejection reasons; improve scanner resolution or provide entry clerk training | +| Batch stuck in `approved` | VigilCareClinical unreachable | Promotion retry worker handles this automatically with exponential backoff | +``` \ No newline at end of file diff --git a/scripts/run-vigilcare-records-verification-p9.sh b/scripts/run-vigilcare-records-verification-p9.sh new file mode 100644 index 0000000..cd3df94 --- /dev/null +++ b/scripts/run-vigilcare-records-verification-p9.sh @@ -0,0 +1,477 @@ +#!/usr/bin/env bash +set -euo pipefail + +# VigilCare Records — End-to-End Verification Script +# Tests the full digitization workflow: upload → entry → verify → approve → promote +# +# Prerequisites: +# - API running on http://localhost:5217 +# - docker compose up -d (PostgreSQL, Redis, MinIO, Seq) +# - Seed data applied (dotnet run applies seed on startup) +# - A test PDF file at ./scripts/test-scan.pdf (create with: echo "test" | enscript -o - | ps2pdf - scripts/test-scan.pdf) +# +# Usage: +# chmod +x scripts/run-vigilcare-records-verification.sh +# ./scripts/run-vigilcare-records-verification.sh + +API="http://localhost:5217/api/v1" +PASS=0 +FAIL=0 +TEST_FILE="${1:-scripts/test-scan.pdf}" + +# Colors for terminal output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_pass() { PASS=$((PASS + 1)); echo -e "${GREEN} PASS${NC} $1"; } +log_fail() { FAIL=$((FAIL + 1)); echo -e "${RED} FAIL${NC} $1"; } +log_step() { echo -e "\n${YELLOW}── $1 ──${NC}"; } + +# Create a minimal test PDF if it doesn't exist +if [ ! -f "$TEST_FILE" ]; then + echo "Creating test PDF at $TEST_FILE..." + mkdir -p "$(dirname "$TEST_FILE")" + echo "%PDF-1.0 +1 0 obj<>endobj 2 0 obj<>endobj 3 0 obj<>>>endobj +xref +0 4 +0000000000 65535 f +0000000009 00000 n +0000000058 00000 n +0000000115 00000 n +trailer<> +startxref +206 +%%EOF" > "$TEST_FILE" +fi + +# ============================================================================ +# Phase 1: Authentication +# ============================================================================ +log_step "1. Authentication" + +# Login as intake clerk +INTAKE_LOGIN=$(curl -s -X POST "$API/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username":"intake1","password":"password"}') +INTAKE_TOKEN=$(echo "$INTAKE_LOGIN" | jq -r '.data.token') +INTAKE_REFRESH=$(echo "$INTAKE_LOGIN" | jq -r '.data.refreshToken') + +if [ "$INTAKE_TOKEN" != "null" ] && [ -n "$INTAKE_TOKEN" ]; then + log_pass "Intake clerk login returned JWT" +else + log_fail "Intake clerk login failed" + exit 1 +fi + +if [ "$INTAKE_REFRESH" != "null" ] && [ -n "$INTAKE_REFRESH" ]; then + log_pass "Intake clerk login returned refresh token" +else + log_fail "Intake clerk login missing refresh token" +fi + +# Login as entry clerk +ENTRY_TOKEN=$(curl -s -X POST "$API/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username":"entry1","password":"password"}' | jq -r '.data.token') + +if [ "$ENTRY_TOKEN" != "null" ] && [ -n "$ENTRY_TOKEN" ]; then + log_pass "Entry clerk login returned JWT" +else + log_fail "Entry clerk login failed" +fi + +# Login as verifier (must be different from entry clerk for separation of duties) +VERIFIER_TOKEN=$(curl -s -X POST "$API/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username":"verifier1","password":"password"}' | jq -r '.data.token') + +if [ "$VERIFIER_TOKEN" != "null" ] && [ -n "$VERIFIER_TOKEN" ]; then + log_pass "Verifier login returned JWT" +else + log_fail "Verifier login failed" +fi + +# Login as approver +APPROVER_TOKEN=$(curl -s -X POST "$API/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username":"approver1","password":"password"}' | jq -r '.data.token') + +if [ "$APPROVER_TOKEN" != "null" ] && [ -n "$APPROVER_TOKEN" ]; then + log_pass "Approver login returned JWT" +else + log_fail "Approver login failed" +fi + +# Verify /me endpoint +ME_ROLE=$(curl -s "$API/auth/me" \ + -H "Authorization: Bearer $INTAKE_TOKEN" | jq -r '.data.role') + +if [ "$ME_ROLE" = "INTAKE_CLERK" ]; then + log_pass "/me returns correct role: INTAKE_CLERK" +else + log_fail "/me returned unexpected role: $ME_ROLE" +fi + +# Verify 401 without token +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$API/auth/me") +if [ "$HTTP_CODE" = "401" ]; then + log_pass "Unauthenticated /me returns 401" +else + log_fail "Unauthenticated /me returned $HTTP_CODE (expected 401)" +fi + +# Refresh token rotation +REFRESH_RESPONSE=$(curl -s -X POST "$API/auth/refresh" \ + -H "Content-Type: application/json" \ + -d "{\"refreshToken\":\"$INTAKE_REFRESH\"}") +NEW_TOKEN=$(echo "$REFRESH_RESPONSE" | jq -r '.data.token') +NEW_REFRESH=$(echo "$REFRESH_RESPONSE" | jq -r '.data.refreshToken') + +if [ "$NEW_TOKEN" != "null" ] && [ -n "$NEW_TOKEN" ] && [ "$NEW_REFRESH" != "$INTAKE_REFRESH" ]; then + log_pass "Refresh rotated tokens successfully" + INTAKE_TOKEN="$NEW_TOKEN" + INTAKE_REFRESH="$NEW_REFRESH" +else + log_fail "Token refresh failed: $REFRESH_RESPONSE" +fi + +# Logout revokes refresh token +LOGOUT_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$API/auth/logout" \ + -H "Content-Type: application/json" \ + -d "{\"refreshToken\":\"$INTAKE_REFRESH\"}") +if [ "$LOGOUT_CODE" = "204" ]; then + log_pass "Logout revoked refresh token (204)" +else + log_fail "Logout returned $LOGOUT_CODE (expected 204)" +fi + +# Re-login for subsequent test phases +INTAKE_LOGIN=$(curl -s -X POST "$API/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username":"intake1","password":"password"}') +INTAKE_TOKEN=$(echo "$INTAKE_LOGIN" | jq -r '.data.token') + +# ============================================================================ +# Phase 2: Upload and Batch Creation +# ============================================================================ +log_step "2. Upload and Batch Creation" + +UPLOAD_RESPONSE=$(curl -s -X POST "$API/digitization-batches" \ + -H "Authorization: Bearer $INTAKE_TOKEN" \ + -F "file=@$TEST_FILE" \ + -F "batchType=VITALS_SHEET" \ + -F "track=BACKFILL") + +BATCH_ID=$(echo "$UPLOAD_RESPONSE" | jq -r '.data.id') +BATCH_STATUS=$(echo "$UPLOAD_RESPONSE" | jq -r '.data.status') + +if [ "$BATCH_ID" != "null" ] && [ -n "$BATCH_ID" ]; then + log_pass "Batch created: $BATCH_ID" +else + log_fail "Batch creation failed: $UPLOAD_RESPONSE" + exit 1 +fi + +if [ "$BATCH_STATUS" = "UPLOADED" ]; then + log_pass "Initial status is UPLOADED" +else + log_fail "Initial status is $BATCH_STATUS (expected UPLOADED)" +fi + +# Verify presigned URL +DOC_URL=$(curl -s "$API/digitization-batches/$BATCH_ID" \ + -H "Authorization: Bearer $INTAKE_TOKEN" | jq -r '.data.documentUrl') + +if [ "$DOC_URL" != "null" ] && [ -n "$DOC_URL" ]; then + log_pass "Presigned URL returned for document" +else + log_fail "No presigned URL returned" +fi + +# ============================================================================ +# Phase 3: Assign and Begin Entry +# ============================================================================ +log_step "3. Assign and Begin Entry" + +# Get entry clerk user ID +ENTRY_USER_ID=$(curl -s "$API/auth/me" \ + -H "Authorization: Bearer $ENTRY_TOKEN" | jq -r '.data.id') + +# Assign batch to entry clerk +ASSIGN_RESPONSE=$(curl -s -X PATCH "$API/digitization-batches/$BATCH_ID/assign" \ + -H "Authorization: Bearer $INTAKE_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"entryClerkUserId\":\"$ENTRY_USER_ID\"}") + +ASSIGN_SUCCESS=$(echo "$ASSIGN_RESPONSE" | jq -r '.success') +if [ "$ASSIGN_SUCCESS" = "true" ]; then + log_pass "Batch assigned to entry clerk" +else + log_fail "Batch assignment failed: $(echo "$ASSIGN_RESPONSE" | jq -r '.error.message')" +fi + +# ============================================================================ +# Phase 4: Draft Data Entry +# ============================================================================ +log_step "4. Draft Data Entry" + +# Save draft patient +PATIENT_RESPONSE=$(curl -s -X PUT "$API/digitization-batches/$BATCH_ID/draft/patient" \ + -H "Authorization: Bearer $ENTRY_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "fullName": "E2E Test Patient", + "dateOfBirth": "1985-06-15", + "sex": "male", + "bloodType": "A+", + "emergencyContact": "Test Contact - 555-9999", + "noKnownAllergies": true + }') + +if [ "$(echo "$PATIENT_RESPONSE" | jq -r '.success')" = "true" ]; then + log_pass "Draft patient saved" +else + log_fail "Draft patient save failed: $(echo "$PATIENT_RESPONSE" | jq -r '.error.message')" +fi + +# Save draft encounter +ENCOUNTER_RESPONSE=$(curl -s -X PUT "$API/digitization-batches/$BATCH_ID/draft/encounter" \ + -H "Authorization: Bearer $ENTRY_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "admissionDate": "2026-06-20T08:00:00Z", + "department": "Emergency Department", + "roomBed": "ED-12", + "admissionReason": "Chest pain, rule out MI" + }') + +if [ "$(echo "$ENCOUNTER_RESPONSE" | jq -r '.success')" = "true" ]; then + log_pass "Draft encounter saved" +else + log_fail "Draft encounter save failed" +fi + +# Add observations +for OBS in \ + '{"observationCode":"HEART_RATE","value":95,"unit":"bpm","recordedAt":"2026-06-20T08:15:00Z"}' \ + '{"observationCode":"TEMP_C","value":37.1,"unit":"C","recordedAt":"2026-06-20T08:15:00Z"}' \ + '{"observationCode":"BP_SYSTOLIC","value":142,"unit":"mmHg","recordedAt":"2026-06-20T08:15:00Z"}' \ + '{"observationCode":"BP_DIASTOLIC","value":88,"unit":"mmHg","recordedAt":"2026-06-20T08:15:00Z"}' \ + '{"observationCode":"RESP_RATE","value":20,"unit":"breaths/min","recordedAt":"2026-06-20T08:15:00Z"}' \ + '{"observationCode":"SPO2","value":97,"unit":"%","recordedAt":"2026-06-20T08:15:00Z"}' +do + OBS_RESULT=$(curl -s -X POST "$API/digitization-batches/$BATCH_ID/draft/observations" \ + -H "Authorization: Bearer $ENTRY_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$OBS") + OBS_CODE=$(echo "$OBS" | jq -r '.observationCode') + if [ "$(echo "$OBS_RESULT" | jq -r '.success')" = "true" ]; then + log_pass "Observation $OBS_CODE added" + else + log_fail "Observation $OBS_CODE failed: $(echo "$OBS_RESULT" | jq -r '.error.message')" + fi +done + +# Verify draft is complete +DRAFT_RESPONSE=$(curl -s "$API/digitization-batches/$BATCH_ID/draft" \ + -H "Authorization: Bearer $ENTRY_TOKEN") + +OBS_COUNT=$(echo "$DRAFT_RESPONSE" | jq -r '.data.observations | length') +if [ "$OBS_COUNT" = "6" ]; then + log_pass "Draft has 6 observations" +else + log_fail "Draft has $OBS_COUNT observations (expected 6)" +fi + +# Submit for verification +SUBMIT_RESPONSE=$(curl -s -X POST "$API/digitization-batches/$BATCH_ID/submit-for-verification" \ + -H "Authorization: Bearer $ENTRY_TOKEN") + +if [ "$(echo "$SUBMIT_RESPONSE" | jq -r '.success')" = "true" ]; then + log_pass "Submitted for verification" +else + log_fail "Submit failed: $(echo "$SUBMIT_RESPONSE" | jq -r '.error.message')" +fi + +# Verify status is now PENDING_VERIFICATION +CURRENT_STATUS=$(curl -s "$API/digitization-batches/$BATCH_ID" \ + -H "Authorization: Bearer $ENTRY_TOKEN" | jq -r '.data.batch.status') + +if [ "$CURRENT_STATUS" = "PENDING_VERIFICATION" ]; then + log_pass "Status is PENDING_VERIFICATION" +else + log_fail "Status is $CURRENT_STATUS (expected PENDING_VERIFICATION)" +fi + +# ============================================================================ +# Phase 5: Separation of Duties Check +# ============================================================================ +log_step "5. Separation of Duties" + +# Entry clerk tries to verify their own batch — should get 409 +SOD_RESPONSE=$(curl -s -X POST "$API/digitization-batches/$BATCH_ID/verify" \ + -H "Authorization: Bearer $ENTRY_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"fieldChecks":[],"passed":true}') + +SOD_CODE=$(echo "$SOD_RESPONSE" | jq -r '.error.code') +if [ "$SOD_CODE" = "SEPARATION_OF_DUTIES_VIOLATION" ]; then + log_pass "Entry clerk cannot verify own batch (409 SEPARATION_OF_DUTIES_VIOLATION)" +else + log_fail "Separation of duties not enforced: $SOD_CODE" +fi + +# ============================================================================ +# Phase 6: Verification +# ============================================================================ +log_step "6. Verification (by different user)" + +VERIFY_RESPONSE=$(curl -s -X POST "$API/digitization-batches/$BATCH_ID/verify" \ + -H "Authorization: Bearer $VERIFIER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "fieldChecks": [ + {"fieldPath": "patient.fullName", "passed": true}, + {"fieldPath": "patient.dateOfBirth", "passed": true}, + {"fieldPath": "observations[0].value", "passed": true}, + {"fieldPath": "observations[1].value", "passed": true}, + {"fieldPath": "observations[2].value", "passed": true}, + {"fieldPath": "observations[3].value", "passed": true}, + {"fieldPath": "observations[4].value", "passed": true}, + {"fieldPath": "observations[5].value", "passed": true} + ], + "passed": true + }') + +if [ "$(echo "$VERIFY_RESPONSE" | jq -r '.success')" = "true" ]; then + log_pass "Verification passed" +else + log_fail "Verification failed: $(echo "$VERIFY_RESPONSE" | jq -r '.error.message')" +fi + +# Check status +VERIFIED_STATUS=$(curl -s "$API/digitization-batches/$BATCH_ID" \ + -H "Authorization: Bearer $VERIFIER_TOKEN" | jq -r '.data.batch.status') + +if [ "$VERIFIED_STATUS" = "VERIFIED" ] || [ "$VERIFIED_STATUS" = "AWAITING_CLINICAL_APPROVAL" ]; then + log_pass "Status after verification: $VERIFIED_STATUS" +else + log_fail "Status after verification: $VERIFIED_STATUS (expected VERIFIED or AWAITING_CLINICAL_APPROVAL)" +fi + +# ============================================================================ +# Phase 7: Approval and Promotion +# ============================================================================ +log_step "7. Approval and Promotion" + +APPROVE_RESPONSE=$(curl -s -X POST "$API/digitization-batches/$BATCH_ID/approve" \ + -H "Authorization: Bearer $APPROVER_TOKEN" \ + -H "Content-Type: application/json" \ + -H "Idempotency-Key: e2e-test-$(date +%s)") + +if [ "$(echo "$APPROVE_RESPONSE" | jq -r '.success')" = "true" ]; then + log_pass "Batch approved and promoted" +else + log_fail "Approval failed: $(echo "$APPROVE_RESPONSE" | jq -r '.error.message')" +fi + +# Verify final status is PROMOTED +FINAL_STATUS=$(curl -s "$API/digitization-batches/$BATCH_ID" \ + -H "Authorization: Bearer $APPROVER_TOKEN" | jq -r '.data.batch.status') + +if [ "$FINAL_STATUS" = "PROMOTED" ]; then + log_pass "Final status is PROMOTED" +else + log_fail "Final status is $FINAL_STATUS (expected PROMOTED)" +fi + +# Verify promotedAt is set +PROMOTED_AT=$(curl -s "$API/digitization-batches/$BATCH_ID" \ + -H "Authorization: Bearer $APPROVER_TOKEN" | jq -r '.data.batch.promotedAt') + +if [ "$PROMOTED_AT" != "null" ] && [ -n "$PROMOTED_AT" ]; then + log_pass "promotedAt timestamp set: $PROMOTED_AT" +else + log_fail "promotedAt is null" +fi + +# ============================================================================ +# Phase 8: Illegal Transition Check +# ============================================================================ +log_step "8. Illegal Transitions" + +# Try to re-approve a promoted batch — should get 409 +ILLEGAL_RESPONSE=$(curl -s -X POST "$API/digitization-batches/$BATCH_ID/approve" \ + -H "Authorization: Bearer $APPROVER_TOKEN" \ + -H "Content-Type: application/json") + +ILLEGAL_CODE=$(echo "$ILLEGAL_RESPONSE" | jq -r '.error.code') +if [ "$ILLEGAL_CODE" = "ILLEGAL_STATUS_TRANSITION" ]; then + log_pass "Cannot approve promoted batch (409 ILLEGAL_STATUS_TRANSITION)" +else + log_fail "Illegal transition not blocked: $ILLEGAL_CODE" +fi + +# ============================================================================ +# Phase 9: Work Queue Overview +# ============================================================================ +log_step "9. Work Queue Overview" + +OVERVIEW_RESPONSE=$(curl -s "$API/work-queue/overview" \ + -H "Authorization: Bearer $APPROVER_TOKEN") + +OVERVIEW_SUCCESS=$(echo "$OVERVIEW_RESPONSE" | jq -r '.success') +if [ "$OVERVIEW_SUCCESS" = "true" ]; then + log_pass "Work queue overview returned successfully" + echo " Status counts: $(echo "$OVERVIEW_RESPONSE" | jq -c '.data.statusCounts')" + echo " Reject rate: $(echo "$OVERVIEW_RESPONSE" | jq -r '.data.rejectRate')" +else + log_fail "Work queue overview failed" +fi + +# ============================================================================ +# Phase 10: Metrics Endpoint +# ============================================================================ +log_step "10. Prometheus Metrics" + +METRICS=$(curl -s http://localhost:5217/metrics) + +if echo "$METRICS" | grep -q "digitization_batches_by_status"; then + log_pass "digitization_batches_by_status metric present" +else + log_fail "digitization_batches_by_status metric missing" +fi + +if echo "$METRICS" | grep -q "digitization_promotion_duration_seconds"; then + log_pass "digitization_promotion_duration_seconds metric present" +else + log_fail "digitization_promotion_duration_seconds metric missing" +fi + +if echo "$METRICS" | grep -q "digitization_rejection_total"; then + log_pass "digitization_rejection_total metric present" +else + log_fail "digitization_rejection_total metric missing" +fi + +# ============================================================================ +# Summary +# ============================================================================ +echo "" +echo "============================================" +echo " E2E Verification Complete" +echo "============================================" +echo -e " ${GREEN}Passed: $PASS${NC}" +echo -e " ${RED}Failed: $FAIL${NC}" +echo "" + +if [ "$FAIL" -eq 0 ]; then + echo -e " ${GREEN}ALL TESTS PASSED${NC}" + exit 0 +else + echo -e " ${RED}$FAIL TEST(S) FAILED${NC}" + exit 1 +fi \ No newline at end of file diff --git a/vigilcare-records-web/.gitignore b/vigilcare-records-web/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/vigilcare-records-web/.gitignore @@ -0,0 +1,24 @@ +# 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/vigilcare-records-web/.vscode/extensions.json b/vigilcare-records-web/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/vigilcare-records-web/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/vigilcare-records-web/README.md b/vigilcare-records-web/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/vigilcare-records-web/README.md @@ -0,0 +1,5 @@ +# 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/vigilcare-records-web/package-lock.json b/vigilcare-records-web/package-lock.json new file mode 100644 index 0000000..747794f --- /dev/null +++ b/vigilcare-records-web/package-lock.json @@ -0,0 +1,2716 @@ +{ + "name": "vigilcare-records-web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vigilcare-records-web", + "version": "0.0.0", + "dependencies": { + "@vueuse/core": "^14.3.0", + "axios": "1.7", + "pinia": "^2.3.1", + "vue": "^3.5.38", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@types/node": "^24.13.2", + "@vitejs/plugin-vue": "^6.0.7", + "@vue/tsconfig": "^0.9.1", + "autoprefixer": "^10.5.2", + "postcss": "^8.5.15", + "tailwindcss": "^3.4.19", + "typescript": "~6.0.2", + "vite": "^8.1.0", + "vue-tsc": "^3.3.5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", + "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", + "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", + "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", + "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", + "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", + "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", + "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", + "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", + "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", + "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", + "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", + "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", + "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", + "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", + "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", + "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.5.tgz", + "integrity": "sha512-UkKu5nhX89fg4VhlG/FOeI10G3cj/7radKT/cy9BT4Q9qJmJlSTAc/dP63Xqs29aypN4f39xUV6PsLNk/dcD6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz", + "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 5.8", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/alien-signals": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", + "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==", + "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.4", + "caniuse-lite": "^1.0.30001799", + "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/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.40", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", + "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "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/electron-to-chromium": { + "version": "1.5.379", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.379.tgz", + "integrity": "sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/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/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/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "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.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "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/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "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.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", + "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.137.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.3", + "@rolldown/binding-darwin-arm64": "1.1.3", + "@rolldown/binding-darwin-x64": "1.1.3", + "@rolldown/binding-freebsd-x64": "1.1.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", + "@rolldown/binding-linux-arm64-gnu": "1.1.3", + "@rolldown/binding-linux-arm64-musl": "1.1.3", + "@rolldown/binding-linux-ppc64-gnu": "1.1.3", + "@rolldown/binding-linux-s390x-gnu": "1.1.3", + "@rolldown/binding-linux-x64-gnu": "1.1.3", + "@rolldown/binding-linux-x64-musl": "1.1.3", + "@rolldown/binding-openharmony-arm64": "1.1.3", + "@rolldown/binding-wasm32-wasi": "1.1.3", + "@rolldown/binding-win32-arm64-msvc": "1.1.3", + "@rolldown/binding-win32-x64-msvc": "1.1.3" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "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/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "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==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz", + "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "~1.1.2", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-tsc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.5.tgz", + "integrity": "sha512-Rzh/G2MmNlMSAMTiQEjDrsb4dgB/jbtEM47rVN2NtidF1dfb/q4w4QvpQBtW5+y3y5H27Hjh7deVwk+YB02fNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.3.5" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + } + } +} diff --git a/vigilcare-records-web/package.json b/vigilcare-records-web/package.json new file mode 100644 index 0000000..1e238b6 --- /dev/null +++ b/vigilcare-records-web/package.json @@ -0,0 +1,29 @@ +{ + "name": "vigilcare-records-web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@vueuse/core": "^14.3.0", + "axios": "1.7", + "pinia": "^2.3.1", + "vue": "^3.5.38", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@types/node": "^24.13.2", + "@vitejs/plugin-vue": "^6.0.7", + "@vue/tsconfig": "^0.9.1", + "autoprefixer": "^10.5.2", + "postcss": "^8.5.15", + "tailwindcss": "^3.4.19", + "typescript": "~6.0.2", + "vite": "^8.1.0", + "vue-tsc": "^3.3.5" + } +} diff --git a/vigilcare-records-web/postcss.config.js b/vigilcare-records-web/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/vigilcare-records-web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/vigilcare-records-web/public/favicon.svg b/vigilcare-records-web/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/vigilcare-records-web/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vigilcare-records-web/public/icons.svg b/vigilcare-records-web/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/vigilcare-records-web/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vigilcare-records-web/src/App.vue b/vigilcare-records-web/src/App.vue new file mode 100644 index 0000000..b28aea4 --- /dev/null +++ b/vigilcare-records-web/src/App.vue @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/vigilcare-records-web/src/api/client.ts b/vigilcare-records-web/src/api/client.ts new file mode 100644 index 0000000..c94d5d9 --- /dev/null +++ b/vigilcare-records-web/src/api/client.ts @@ -0,0 +1,173 @@ +import axios, { type AxiosInstance, type InternalAxiosRequestConfig } from 'axios' +import type { ApiResponse, TokenResponse } from '../types' + +const REFRESH_BUFFER_MS = 60_000 // refresh 1 minute before access token expiry + +const apiClient: AxiosInstance = axios.create({ + baseURL: '/api/v1', + headers: { + 'Content-Type': 'application/json', + }, + timeout: 30000, +}) + +function getStoredAccessToken(): string | null { + return localStorage.getItem('vigilcare_token') +} + +function getStoredRefreshToken(): string | null { + return localStorage.getItem('vigilcare_refresh_token') +} + +function setStoredTokens(accessToken: string, refreshToken: string): void { + localStorage.setItem('vigilcare_token', accessToken) + localStorage.setItem('vigilcare_refresh_token', refreshToken) +} + +function clearStoredAuth(): void { + localStorage.removeItem('vigilcare_token') + localStorage.removeItem('vigilcare_refresh_token') + localStorage.removeItem('vigilcare_user') +} + +function decodeJwtExpiry(token: string): number | null { + try { + const payload = JSON.parse(atob(token.split('.')[1]!)) + return typeof payload.exp === 'number' ? payload.exp * 1000 : null + } catch { + return null + } +} + +let refreshPromise: Promise | null = null + +async function refreshAccessToken(): Promise { + const refreshToken = getStoredRefreshToken() + if (!refreshToken) return null + + if (!refreshPromise) { + refreshPromise = (async () => { + try { + const response = await axios.post>( + '/api/v1/auth/refresh', + { refreshToken } + ) + const data = response.data.data + if (!response.data.success || !data) return null + + setStoredTokens(data.token, data.refreshToken) + scheduleProactiveRefresh(data.token) + return data.token + } catch { + return null + } finally { + refreshPromise = null + } + })() + } + + return refreshPromise +} + +let refreshTimer: ReturnType | null = null + +function scheduleProactiveRefresh(accessToken: string): void { + if (refreshTimer) clearTimeout(refreshTimer) + + const expiresAt = decodeJwtExpiry(accessToken) + if (!expiresAt) return + + const delay = Math.max(expiresAt - Date.now() - REFRESH_BUFFER_MS, 0) + refreshTimer = setTimeout(() => { + void refreshAccessToken() + }, delay) +} + +export function initializeAuthRefresh(): void { + const token = getStoredAccessToken() + if (token) scheduleProactiveRefresh(token) +} + +function redirectToLogin(): void { + clearStoredAuth() + window.location.href = '/login' +} + +// Request interceptor: attach JWT from localStorage +apiClient.interceptors.request.use( + async (config: InternalAxiosRequestConfig) => { + const token = getStoredAccessToken() + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => Promise.reject(error) +) + +// Response interceptor: retry once on 401 after refresh +apiClient.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean } + + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true + const newToken = await refreshAccessToken() + if (newToken && originalRequest.headers) { + originalRequest.headers.Authorization = `Bearer ${newToken}` + return apiClient(originalRequest) + } + redirectToLogin() + } + + return Promise.reject(error) + } +) + +// Typed API helpers +export async function get(url: string, params?: Record): Promise> { + const response = await apiClient.get>(url, { params }) + return response.data +} + +export async function post(url: string, data?: unknown): Promise> { + const response = await apiClient.post>(url, data) + return response.data +} + +export async function put(url: string, data?: unknown): Promise> { + const response = await apiClient.put>(url, data) + return response.data +} + +export async function patch(url: string, data?: unknown): Promise> { + const response = await apiClient.patch>(url, data) + return response.data +} + +export async function del(url: string): Promise> { + const response = await apiClient.delete>(url) + return response.data +} + +// Multipart upload helper — used by IntakeView for document upload +export async function uploadFile( + url: string, + file: File, + fields: Record +): Promise> { + const formData = new FormData() + formData.append('file', file) + Object.entries(fields).forEach(([key, value]) => { + formData.append(key, value) + }) + + const response = await apiClient.post>(url, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + timeout: 60000, // uploads may be larger — extend timeout + }) + return response.data +} + +export default apiClient \ No newline at end of file diff --git a/vigilcare-records-web/src/assets/hero.png b/vigilcare-records-web/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 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 P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/vigilcare-records-web/src/assets/main.css b/vigilcare-records-web/src/assets/main.css new file mode 100644 index 0000000..2e49b0b --- /dev/null +++ b/vigilcare-records-web/src/assets/main.css @@ -0,0 +1,44 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer components { + .btn-primary { + @apply bg-primary-600 text-white px-4 py-2 rounded-md + hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed + transition-colors; + } + .btn-danger { + @apply bg-clinical-danger text-white px-4 py-2 rounded-md + hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed + transition-colors; + } + .form-input { + @apply block w-full rounded-md border border-gray-300 px-4 py-2 + focus:border-primary-500 focus:ring-2 focus:ring-primary-500 + disabled:bg-gray-100 disabled:text-gray-500; + } + .page-container { + @apply p-4 sm:p-6 lg:p-8 max-w-4xl mx-auto; + } + .app-header { + @apply flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between + px-4 py-4 sm:px-6 sm:py-4 bg-white border-b shadow-sm; + } + .app-header-title { + @apply flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4; + } + .app-header-actions { + @apply flex flex-wrap items-center gap-2 sm:gap-4; + } + .card { + @apply bg-white rounded-lg shadow-md p-4 sm:p-6; + } + .split-pane { + @apply grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-8 + h-auto lg:h-[calc(100vh-4rem)] min-h-0; + } + .status-badge { + @apply px-2 py-1 rounded-full text-xs font-medium; + } +} diff --git a/vigilcare-records-web/src/assets/vite.svg b/vigilcare-records-web/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/vigilcare-records-web/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/vigilcare-records-web/src/assets/vue.svg b/vigilcare-records-web/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/vigilcare-records-web/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vigilcare-records-web/src/components/AppHeader.vue b/vigilcare-records-web/src/components/AppHeader.vue new file mode 100644 index 0000000..180de55 --- /dev/null +++ b/vigilcare-records-web/src/components/AppHeader.vue @@ -0,0 +1,27 @@ + + + diff --git a/vigilcare-records-web/src/components/AssignClerkDialog.vue b/vigilcare-records-web/src/components/AssignClerkDialog.vue new file mode 100644 index 0000000..1814e2c --- /dev/null +++ b/vigilcare-records-web/src/components/AssignClerkDialog.vue @@ -0,0 +1,95 @@ + + + diff --git a/vigilcare-records-web/src/components/BatchList.vue b/vigilcare-records-web/src/components/BatchList.vue new file mode 100644 index 0000000..651ec1b --- /dev/null +++ b/vigilcare-records-web/src/components/BatchList.vue @@ -0,0 +1,98 @@ + + + diff --git a/vigilcare-records-web/src/components/EntryForm.vue b/vigilcare-records-web/src/components/EntryForm.vue new file mode 100644 index 0000000..7939114 --- /dev/null +++ b/vigilcare-records-web/src/components/EntryForm.vue @@ -0,0 +1,273 @@ + + + \ No newline at end of file diff --git a/vigilcare-records-web/src/components/ObservationRow.vue b/vigilcare-records-web/src/components/ObservationRow.vue new file mode 100644 index 0000000..ce27792 --- /dev/null +++ b/vigilcare-records-web/src/components/ObservationRow.vue @@ -0,0 +1,110 @@ + + + \ No newline at end of file diff --git a/vigilcare-records-web/src/components/PatientSearch.vue b/vigilcare-records-web/src/components/PatientSearch.vue new file mode 100644 index 0000000..af39092 --- /dev/null +++ b/vigilcare-records-web/src/components/PatientSearch.vue @@ -0,0 +1,71 @@ + + + diff --git a/vigilcare-records-web/src/components/ScanViewer.vue b/vigilcare-records-web/src/components/ScanViewer.vue new file mode 100644 index 0000000..61e2583 --- /dev/null +++ b/vigilcare-records-web/src/components/ScanViewer.vue @@ -0,0 +1,113 @@ +