diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..ef6a144 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "model": "opus" +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1091eea --- /dev/null +++ b/.env.example @@ -0,0 +1,51 @@ +# ── API ────────────────────────────────────────────────────────────── +PORT=3000 +# REQUIRED in production — boot fails on dev defaults when NODE_ENV=production. +JWT_SECRET=change-me +JWT_EXPIRES=8h +# Pin in production, e.g. CORS_ORIGIN=https://edge.ubi-as.com (comma-separated ok). +CORS_ORIGIN= +# Demo seed (PUBLIC passwords) — dev/staging only. NEVER true in production. +SEED_DEMO=true +# Field roles get a long offline grace; sensitive roles short sessions. +JWT_EXPIRES_FIELD=72h + +# ── Stateful endpoints — EXTERNALIZED FROM LINE ONE (hard rule 6) ──── +# Co-located on one box today, but always addressed as endpoints so the +# stack splits later via config, not code. +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_USER=ubi +DB_PASS=ubi_dev +DB_NAME=ubi_suite +# Dev convenience only — use migrations before production. +DB_SYNC=true + +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 + +# ── Storage (hard rule 3: binaries never in Postgres) ──────────────── +# Swappable driver: local now → drive/s3/hci later, no rewrite. +STORAGE_DRIVER=local +STORAGE_ROOT=./uploads + +# ── Sign in with Google (optional, layered on standalone auth) ─────── +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +# ── Cloudflare Turnstile (public helpdesk form) ────────────────────── +# Server-side secret. When set, /api/public/helpdesk REQUIRES a valid +# captcha token. The matching site key goes to the web app: +# apps/web/.env → VITE_TURNSTILE_SITE_KEY= +TURNSTILE_SECRET_KEY= + +# ── External systems (integration stubs) ───────────────────────────── +ACUMATICA_BASE_URL= +OAEC_BASE_URL= + +# ── Cartrack Fleet API (live GPS / fleet management) ───────────────── +# Base: https://fleetapi-.cartrack.com/rest · HTTP Basic auth. +# Without USER+PASS the Fleet module serves a labeled demo fleet. +CARTRACK_REGION=ph +CARTRACK_USER= +CARTRACK_PASS= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..744f64d --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# deps & build +node_modules/ +dist/ +build/ +.vite/ +.turbo/ +coverage/ + +# env & secrets (never commit) +.env +.env.* +!.env.example + +# local file storage — binaries go through StorageProvider, never git +uploads/ + +# misc +*.log +.DS_Store +*.local diff --git a/AUDIT.md b/AUDIT.md new file mode 100644 index 0000000..d9d90ad --- /dev/null +++ b/AUDIT.md @@ -0,0 +1,88 @@ +# Feature Audit vs Original Spec — updated 2026-06-16 + +> Tracks modules against `CLAUDE.md` Part II. + +## Legend +✅ built · 🟡 stub / read-surface only · ❌ not started + +--- + +## Production hardening (CLAUDE.md §0 item 1) +| Item | Status | Notes | +|---|---|---| +| Helmet security headers | ✅ | added to `main.ts` — all responses get standard security headers | +| TypeORM migration infrastructure | ✅ | `data-source.ts` + `migration:generate/run/revert` scripts in `apps/api/package.json`; **DB_SYNC must be set false and migrations run before production** | +| TypeORM initial migration file | ❌ | run `npm run migration:generate` against a live DB once to capture the current schema | +| Nginx/systemd/deploy docs | ✅ | `ops/nginx.sample.conf`, `ops/systemd/`, `ops/DEPLOY.md`, `ops/INSTALL-AUTOMATION.md` | +| Postgres + uploads backups | ❌ | not started — add a cron/systemd timer calling `pg_dump` + rsync uploads/ | +| Pre-prod audit checklist | ✅ | `ops/PRE-PROD-AUDIT.md` — rotate secrets, disable demo seed, verify RBAC | + +--- + +## Survey module — ✅ real (this session) +| Spec feature | Status | Notes | +|---|---|---| +| Monthly volume feed to ManCom | ✅ | `survey.volumes` read-model pulls from real `survey_measurement` table | +| Cross-section data capture | ✅ | `survey_cross_section` entity + endpoints | +| Station/chainage indexing | ✅ | `station_start` / `station_end` in STA 0+000 format | +| As-built / original-ground / stakeout types | ✅ | `type` field on each measurement | +| Web page | ✅ | `/survey` — volume trend chart, measurement table by project, submit form | +| Demo seed | ✅ | 8 measurements for PKG-02 (PCCP) and PKG-05 (subbase) | + +## MQC module — ✅ real (this session) +| Spec feature | Status | Notes | +|---|---|---| +| DPWH minimum testing rules | ✅ | `DPWH_TEST_RULES` table for items 311(1)a/b/c, 200, 201, 301; `GET /mqc/rules/:payItemNo` | +| QC test results | ✅ | `mqc_test` entity + endpoints; `mqc.certs` read-model (pending = blocking billing) | +| Pour logs (standardised form) | ✅ | `mqc_pour_log` — station, mix design, volume, slump, air content | +| Material certificates | ✅ | `mqc_material_cert` entity + issue endpoint; `billingRef` links to Engineering billing | +| Web page | ✅ | `/mqc` — DPWH rules panel, test results tab, pour logs tab, submit forms | +| Demo seed | ✅ | 5 test results + 2 pour logs for PKG-02 / PKG-05 | + +--- + +## IT module +| Spec feature | Status | Notes | +|---|---|---| +| AI Helpdesk (landing, queue) | ✅ | Public no-signin form + Turnstile gate; feeds shared ticketing | +| Capacity planning & controls | 🟡 | Read-model with storage/transfer/DB/UPS rows + thresholds (DB-backed stubs) | +| SNMP device monitoring + map | ❌ | Device list is real; live polling / topology map not started | +| DeskGuard v2 (client OCR + KPIs) | ❌ | Not started | +| Maintenance scheduler (PMS) | ✅ | Auto-generated from Property assets; web page at `/it/pms` | +| Environmental monitoring (DHT22/ESP32) | ❌ | Not started | + +## Property module — ✅ real QR EAM +`asset` + `asset_movement` + `custodian_attestation` tables. Scan endpoint, monthly attestation, history. Tools at `/property`. + +## Records module — ✅ real +`vehicle_doc` + expiry engine + `physical_location` index (access-gated by claim). Tools at `/records`. + +## Department dashboards — ✅ all 14 +Standard KPI band + dept Pillar-3 read model + CCTV tile. CCTV goes live when `CCTV_BASE_URL` is set. + +## Fleet (Cartrack) — ✅ integrated +Live GPS map (Leaflet), vehicle list, trips/events/geofences/drivers/fuel/maintenance proxy. ManCom live-fleet card. + +## Engineering — ✅ core + quantity +Projects, DPWH pay-item library, weekly materials schedule (AI-prefilled), PE→PM→VPO approvals, quantity entry/projection/head pages. + +--- + +## Still stub / not started +- Audit, Clinic, Admin, HR, Finance — read-surface stubs only (render in dept dashboards; deep workflows pending) +- IT SNMP live polling + topology map +- IT DeskGuard v2 +- Engineering: SWA/billing tables — `engineering.swa` needs real persistence +- Field photo capture pipeline (GPS + timestamp + <500 KB compress + offline queue) +- Google OAuth +- Offline cached login (field PWA grace) +- TypeORM initial migration file (needs live DB to generate) +- Postgres + uploads backup script + +## Suggested next build order +1. **Engineering SWA + billing** — real `weekly_swa` and `billing_batch` tables; SWA feeds ManCom volumes. +2. **Audit module** — findings register + exception tracker (directly feeds AI Insights). +3. **IT SNMP live polling** — SNMP-trap / polling worker + topology map. +4. **Field photo pipeline** — client-side compress + GPS/timestamp + offline queue service worker. +5. **Google OAuth** — layer on top of standalone auth. +6. **Postgres + uploads backup script** — cron/systemd timer + pg_dump + rsync. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1ab8175 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,821 @@ +# UBI Construction Suite — Master Context (CLAUDE.md) + +> **Single-file kickoff context — read top to bottom.** Part I defines the stack, +> the three cross-cutting pillars, the hard rules, and the conventions. Part II +> contains the three pillars in full plus all 14 module specs. When any single +> module's spec grows large during the build, split just that one out into its +> own file and leave a pointer here. + +--- + +## 0. CURRENT BUILD STATE — START HERE (updated 2026-06-18) + +> **For Claude Code sessions:** the suite is **no longer pre-code**. Working +> code is on branch `claude/magical-darwin-dulx3f` (same tip as `production`). +> Live at `edge.ubi-as.com`. **Do not rebuild anything — continue from here.** + +### Deploy state (read this first) +- **Feature branch tip:** `d56c42e` (docs: clarify CD chain) +- **Production branch tip:** `d56c42e` — same, all code is pushed. +- **Last confirmed box deploy:** `ae7aff5` (Cartrack env apply). Commits + `c2f83f2` → `d56c42e` (Survey, MQC, Omega module, fuel level) are on + `production` branch but **may still be building** — the owner ran + `bash ops/wake-claude-deploy.sh --force` just before this handoff. +- **Verify at session start:** `curl -s https://edge.ubi-as.com/api/health` + then check `git show origin/production:ops/DEPLOY_STATUS.md`. If result + is still `NOT-YET-DEPLOYED` or `deployed_sha` < `d56c42e`, the 1-min + timer will pick it up automatically — just wait or run `--force` again. +- **Login works:** confirmed `POST /api/auth/login` → 200. Credentials: + `ceo@ubi.ph` / `Ulticon1` (demo). Browser DNS issue noted (see below). +- **CD chain:** `ubi-deploy.timer` is active on the box (running since + 2026-06-15). GitHub webhook receiver is also up (`/ubi-deploy-hook` → 405). + GitHub-side webhook is NOT yet registered — timer alone covers deploys + (1-min lag). To push a deploy: `git push origin HEAD:production`. + +### Known issue — client DNS (ERR_NAME_NOT_RESOLVED) +The owner's browser gets `ERR_NAME_NOT_RESOLVED` for `edge.ubi-as.com`. +Server-side DNS resolves fine (IP `13.229.193.27`). Fix on client: +`ipconfig /flushdns` or set DNS to `8.8.8.8`. This is NOT a code issue. + +**What exists and runs** +- Monorepo: `apps/api` (NestJS + Helmet), `apps/web` (React+Vite PWA), + `packages/types`, `packages/storage`. +- **Pillars working:** auth (JWT + claims RBAC + entity scope, field-role + grace), doc tracking (QR codes, scan transmit/receive, where-is + history), + AI layer (read-model registry — 21+ surfaces, `/api/ai/ask`, off-box insight + postback at `/api/ai/insights`). +- **Shared engines:** approvals (PE→PM→VPO chains), ticketing (IT+Admin), + notifications, expiry reminders, RBAC-gated `/api/files/:id` over + `StorageProvider` (LocalDisk). +- **Real modules:** Engineering core (projects, DPWH pay-item library, + AI-prefilled weekly materials schedule → approvals), Procurement DR capture, + Operations control tower, CEO ManCom consolidation, **Survey** (measurements + + cross-sections + station/chainage), **MQC** (DPWH test rules, QC tests, pour + logs, material certs), Property (QR EAM), Records (vehicle doc registry, + physical-location index), IT (SNMP device list, PMS scheduler, DeskGuard + consent, env monitoring), **Omega/Fleet** (Cartrack server-side poller 10s, + analytics DB snapshots, equipment register, fuel level + low-fuel alerts, + `/fleet/*` + `/omega/equipment` routes). +- **Web:** "drawing comes alive" design system. Pages: login, ManCom, AI + Insights, Ask AI, Documents, Approvals, Engineering, Quantity, Equipment, + Survey, MQC, Property, Records, IT (+ PMS), Omega/Fleet (live map + tabs). + +**Run locally** — see README. Short: `cp .env.example .env` → Postgres+Redis +up → `npm install` → `npm run dev:api` (:3000; seeds demo data only when +`SEED_DEMO=true` and DB is empty) → `npm run dev:web` (:5173). + +**Production env (edge.ubi-as.com):** `JWT_SECRET` is **required** (boot +fails on the dev default), `SEED_DEMO` must be unset/false, set +`CORS_ORIGIN=https://edge.ubi-as.com`. Before production: run TypeORM +migrations (`npm run migration:generate` then `migration:run`) and set +`DB_SYNC=false`. See `ops/PRE-PROD-AUDIT.md`. + +**Next work, in order** +1. **Verify Omega deploy** — confirm `/api/omega/equipment` and `/api/fleet/live` + are live on the box after the in-progress deploy completes. +2. **Register GitHub webhook** — `ops/INSTALL-AUTOMATION.md` step 2 on the box + so future `production` pushes deploy instantly (not just via 1-min timer). +3. **Postgres + uploads backup script** — cron/systemd + `pg_dump` + rsync. +4. **Engineering SWA + billing** — real `weekly_swa` and `billing_batch` + tables; SWA feeds ManCom volumes. +5. **Audit module** — findings register + exception tracker feeds AI Insights. +6. **IT SNMP live polling** — actual SNMP worker + topology map. +7. **Field photo pipeline** — client-side compress + GPS/timestamp + offline + queue service worker. +8. **Google OAuth** — layer on top of standalone auth. + +Micro-commits, conventional scopes per §11. + +> **⚠️ Before any real production launch: read `ops/PRE-PROD-AUDIT.md`** and walk +> it. Pilot shortcuts to clear — rotate the Cartrack admin key (in git history), +> the GitHub PAT, and the shared `Ulticon1` password; migrations; backups; etc. +> If the owner mentions "audit before production," raise that checklist. + +### Deploying (Claude-driven CD — see `ops/README.md`) +- Dev happens on the feature branch (e.g. `claude/magical-darwin-dulx3f`) → push + to that branch → **to deploy: push the same commit to `production`**. +- **`production` branch IS the deploy signal.** Server Claude on the box + (`edge.ubi-as.com`) polls `production` via a 1-min systemd timer + (`ubi-deploy.timer`) **and** a GitHub push webhook (`/ubi-deploy-hook`). + Either triggers `ops/wake-claude-deploy.sh` → headless `claude -p` → + reads `ops/DEPLOY.md` → `git pull` → `npm install` → `npm run build` → + `systemctl restart ubi-edge` → health check → pushes `ops/DEPLOY_STATUS.md`. +- **"build production"** = Cloud Claude fast-forwards `production` to the current + feature branch tip: `git push origin HEAD:production` (note any env/migration + steps in `ops/RELEASE_NOTES.md` first). +- At session start, Cloud Claude should read `ops/DEPLOY_STATUS.md` and act on + its `for_cloud_claude:` line. + +--- + +## 1. What this is + +A **full construction management suite for Ulticon Builders, Inc. (UBI)** that +covers everything the **Acumatica ERP cannot** do. Acumatica remains the +financial/ERP system of record. This suite owns field capture, departmental +workflows, document movement, and executive visibility — and **integrates** with +Acumatica rather than duplicating it. + +- **Entities:** two — **UBI** and **Omega Asia Equipment Leasing Corp**. All data + is entity-scoped from day one. Some flows cross entities (Omega leases + equipment to UBI). +- **Projects:** 100+ active, **all DPWH government contracts**, **standard DPWH + Blue Book pay items** (Item 100s clearing, 200s subbase, 300s/311 PCCP, 400s + structures, SPL items). One master pay-item library, reused everywhere. +- **Users:** ~500 total, **~50 concurrent**. Field-heavy. Mobile-first. +- **Primary output:** the **ManCom dashboard** for the CEO/President — every + department ultimately feeds it. + +--- + +## 2. The stack (locked — do not improvise) + +| Layer | Choice | +| ---------------- | ------------------------------------------------------------- | +| Language | **TypeScript end-to-end** | +| Backend | **Node.js** (NestJS — modular monolith) | +| Frontend | **React + Vite, PWA**, mobile-first, offline-capable for field| +| Database | **PostgreSQL** | +| Cache/queue | **Redis** (sessions, BullMQ job queue) | +| Reverse proxy | **Nginx** | +| Host | AWS Lightsail Ubuntu — **4 vCPU / 16 GB / 320 GB SSD** | + +**Architecture:** a **modular monolith**, not microservices. One deployable Node +app with clean internal module boundaries. Small team; one language everywhere. + +**The box is pure API/backend.** All AI agents run **off-box** (head-office HCI +or workstations) and call into the API. The box never runs local AI inference or +local OCR. + +> OAEC (Omega's existing system) now shares this stack (Node/Ubuntu). Capture, +> sync, and AI-encode patterns are shared concepts; the equipment-schedule flow +> from UBI integrates to OAEC via API. + +--- + +## 3. HARD RULES (these are architectural constraints, not suggestions) + +1. **No AI inference on the API box.** All vision/OCR/LLM work happens on + off-box agents (HCI/workstations) or the Anthropic API. The box stores and + serves results only. +2. **DeskGuard OCR is client-side.** Agents on company PCs extract screen text + + keystroke/mouse counts locally and POST small JSON. Screenshots are optional, + sampled, and compressed. +3. **Binaries never live in Postgres.** Postgres stores **metadata + path/id + only**. Files go through a **swappable `StorageProvider` interface** + (LocalDisk now → Drive/S3/HCI later, no rewrite). +4. **Files are served through the API with RBAC** (`/files/:id`), never via raw + filesystem paths or public links. Access control is enforced at fetch time. +5. **Edge compression is mandatory.** Field photos compressed **client-side to + <500 KB** (resize longest edge ~1600px, JPEG q~75, **keep EXIF GPS + + timestamp**). DeskGuard screenshots **<200 KB**, sampled only. +6. **Externalize stateful endpoints from line one.** DB, Redis, and storage are + configured as external endpoints even while co-located on one box, so the + stack splits cleanly later (and re-points at HCI in the hybrid phase) via + config, not code. +7. **Run heavy migrations/builds off-hours.** Lightsail CPU is burstable; + sustained CPU during business-critical windows (billing/payroll cutoffs) + must be avoided. + +--- + +## 4. The three pillars (cross-cutting cores — build before modules) + +Every module depends on these. They are not features of any one department. + +### Pillar 1 — Identity & Access (see Part II) +- Standalone auth (app owns the user table + RBAC) as the base. +- **Sign in with Google** (OAuth) as an optional method layered on top, for + Workspace users. Email/username + password for everyone else. Accounts can + link both. +- **No LDAP** as primary (head-office only, unreachable from cloud/sites). + Revisit only in the hybrid phase if ever needed. +- **Offline cached login** for field PWAs: after first online login, device + caches an encrypted session + profile with a long offline grace window. Field + roles get extended grace; sensitive office roles (Finance, HR, Clinic) get + short sessions. +- **Entity-scoped** (UBI/Omega) + RBAC: `dept:*`, `role:manager`, + `scope:project-`. CEO/VPO see across; managers see their dept. + +### Pillar 2 — Document Tracking & Transmittal (see Part II) +**The backbone of the suite.** Huge volume of paper moves between departments. +- Every physical document gets an **ID + QR label + current holder + location + + full movement history**. +- **Scan to transmit / scan to receive.** Answer "where is this document right + now?" instantly. +- Used by **every** department: transmittals, PRs, payment requests, billing + attachments, doc batches to cashier, incoming from Records, etc. +- Interim reality: paper-first with scan-and-route + transmittal tracking → + end-state digital forms. + +### Pillar 3 — AI-Queryable Data Layer (see Part II) +- **Every module exposes its data through a consistent, queryable surface** so + the CEO chat, AI insights, and ManCom consolidation work across all modules. +- Design every module's read model with this in mind from the start. +- Off-box agents consume this surface; the **agentic ManCom iframe** and + **CEO conversational AI** are clients of it. + +--- + +## 5. Shared services (build once, reuse everywhere) + +| Service | Used by | +| --------------------------- | ---------------------------------------------------- | +| **StorageProvider** | all modules with photos/files (LocalDisk first) | +| **Photo-capture pipeline** | DR/receiving, progress, billing photos, QR scans | +| (GPS + timestamp + compress + queue + sync; OAEC/ZeroType pattern) | | +| **Approvals engine** | materials schedule, subcontracts, PRs, etc. | +| **Ticketing engine** | IT helpdesk **and** Admin inquiries (one engine) | +| **Expiry/Renewal reminders**| Records (vehicle reg, insurance), Admin, Property, IT | +| **Notification service** | approvals, expiries, alerts, ManCom flags | + +--- + +## 6. External systems + +| System | Role | Integration | +| ------------ | -------------------------------------- | ------------------------------------ | +| **Acumatica**| ERP system of record (GL, AP/AR, PO) | REST/OData; AI-encoded DR/receiving flows **into** it | +| **Ezacctg** | Payroll/201/statutory SoR (on-prem) | File/DB exchange. We add the **hours-computation layer** it lacks, feed clean totals back | +| **OAEC** | Omega fleet system (Node/Ubuntu) | API — receives UBI equipment schedule | +| **BSmart** | Fuel dispensers | (fuel domain; pairs with OAEC later) | + +**Never duplicate financial truth.** Each module must state explicitly what +lives in the suite vs. what syncs to Acumatica. + +--- + +## 7. Conventions + +- **Monorepo.** `apps/api` (NestJS), `apps/web` (React PWA), `packages/*` for + shared libs (storage, capture, auth-client, ui, types). +- **TypeScript strict.** Shared types in `packages/types` — API and web import + the same contracts. +- **Every table is entity-scoped** (`entity_id`) and carries `created_by`, + `created_at`, `updated_at`, soft-delete where audit matters. +- **Pay items are a shared reference** (`pay_item` master) — Quantity, Billing, + MQC, SWA, Survey all reference item numbers. +- **All writes that matter are auditable** (Audit module reads across). +- **PWA offline:** queue-and-sync for all field captures; never block the user + on connectivity. +- **Secrets via env vars only.** Never commit secrets. + +--- + +## 8. Module index + +Office/desktop + field/mobile per module. Full specs are in Part II below. + +| Module | Context file | +| ------------------------------ | ------------------------------------- | +| Engineering (Bidding, Financial Planning, Quantity, Billing, Contracts, Archiving) | see Part II | +| Procurement | see Part II | +| Operations (VPO control tower) | see Part II | +| Survey | see Part II | +| MQC (Materials Quality Control)| see Part II | +| Audit (+ Exception register) | see Part II | +| IT (SNMP, DeskGuard v2, Helpdesk, PMS, Env) | see Part II | +| Records (+ physical index, expiry) | see Part II | +| Clinic (isolated, encrypted) | see Part II | +| Admin / OHS (site gateway) | see Part II | +| HR (thin — hours engine) | see Part II | +| Property (QR EAM) | see Part II | +| Finance (doc-tracking consumer)| see Part II | +| CEO / Office of the President | see Part II | + +--- + +## 9. Phasing (suggested build order — dependency-driven) + +1. **Pillars first:** Auth → Document Tracking → AI-Queryable layer scaffold + + StorageProvider + capture pipeline. +2. **Engineering** (projects, pay-item library, financial planning, quantity, + SWA, billing) — defines the data everything references. +3. **Field capture** (materials schedule, DR/receiving with AI encode, + progress photos) + **Procurement** + **Operations** approvals. +4. **Survey + MQC** (feed SWA, ManCom volumes, billing certs). +5. **Property** (QR EAM) + **Records** + **Admin/OHS** + **Clinic**. +6. **IT module** (helpdesk, SNMP map, DeskGuard v2, PMS, env monitoring). +7. **HR hours engine** → Ezacctg. +8. **CEO / ManCom dashboard** (live consolidation + AI insights + agentic + iframe + trend projection) — last, sits over everything. + +--- + +## 10. Capacity note + +50 concurrent on transactional load is comfortable on this box **because** AI is +off-box, OCR is client-side, files are compressed + served lean, and Postgres +holds only structured data + metadata. Scale-out trigger: fleet-wide DeskGuard + +all field modules live → split Postgres to its own instance, then add a standby +(month-6 conversation). The externalized-endpoints rule (HARD RULE 6) makes that +a config change. + +--- + +## 11. Git & branching strategy (Jules White style) + +**Repo:** `github.com/mcabico-blip/ubi-construction-suite` + +**Principle:** isolate work by module, keep AI-session branches small and +short-lived, merge upward through clean PRs. This mirrors the phasing — pillars +first, then modules — and keeps history readable. + +### Branch layers +1. **`main`** — protected, always-releasable. No direct commits; merge via PR only. +2. **One long-lived branch per module/pillar**, named by the module: + `pillar-auth`, `pillar-doc-tracking`, `pillar-ai-layer`, + `engineering`, `procurement`, `operations`, `survey`, `mqc`, `audit`, + `it`, `records`, `clinic`, `admin`, `hr`, `property`, `finance`, `ceo`. +3. **Feature / sub-branches** off the module branch, named: + **`__`** (lowercase; hyphens inside the + feature name, underscores between the three parts). + +### Naming examples +- `engineering_billing_monthly-dpwh` +- `engineering_materials_weekly-schedule-ai` +- `procurement_receiving_dr-ai-encode` +- `it_capacity_storage-forecast` +- `it_deskguard_clientside-ocr` +- `property_qr_custodian-attestation` +- `ceo_mancom_trend-projection` + +### Flow +`__` → PR into its **module branch** → when the +module reaches a stable slice → PR module branch → **`main`**. + +### Commits (conventional, scoped) +- `feat(engineering/billing): monthly DPWH billing draft` +- `fix(it/capacity): correct storage trend forecast` +- `chore(pillar-auth): add offline session cache` + +### Rules +- **One Claude Code working session = one feature branch.** Keep it small. +- Never commit secrets or binaries — files go through `StorageProvider`, not git + (see HARD RULES). `uploads/` and `.env*` are git-ignored. +- Tag releases on `main`: `v0.1.0-pillars`, `v0.2.0-engineering`, etc. + +--- + +# PART II — PILLARS & MODULE SPECS + +> The sections below were previously separate context files. Merged here for +> kickoff. If a single module's spec grows large during the build, split just +> that one out into its own file and leave a pointer here. + + +--- + +## Pillar 1 — Identity & Access + +### Model +- App owns the **user table + RBAC** (standalone is the base, the RBAC anchor). +- **Sign in with Google** (OAuth) layered on top for Workspace users; email/ + username + password for everyone else. One account can link both. Admins can + later enforce Google-only for office staff. +- **No LDAP as primary** (head-office only, unreachable from cloud/sites). + Revisit only in hybrid phase. + +### Offline cached login (field PWAs) +- After first **online** login, device caches an **encrypted session + profile**. +- Long offline grace for **field roles**; short sessions for **Finance, HR, + Clinic** (sensitive). +- User opens app offline, works, queues captures; everything syncs under the + cached identity when signal returns. + +### Scoping & roles +- **Entity-scoped:** UBI / Omega on every record. +- Roles/claims: `dept:`, `role:manager`, `scope:project-`. +- CEO/VPO: cross-cutting read. Managers: their dept. Field: their project scope. + + +--- + +## Pillar 2 — Document Tracking & Transmittal (BACKBONE) + +The single most-used service in the suite. Lots of paper moves between depts. + +### Core +- Every physical document = **ID + QR label + current holder + location + + movement history**. +- **Scan to transmit / scan to receive** (mobile, edge). +- Instant answer to **"where is this document right now?"** for CEO/VPO. + +### Consumers (everyone) +- Engineering transmittals, PRs, payment requests, billing attachments. +- Finance: doc batches/boxes to cashier; incoming from Records + other depts. +- Records: incoming/outgoing correspondence, controlled docs. +- Property/Admin/etc.: any routed paper. + +### Phases +- **Now:** paper-first — scan-and-route + transmittal tracking + soft-copy + attachments where possible. +- **End-state:** digital forms replace paper where feasible. + +### Data +- `document` (id, entity_id, type, title, current_holder, current_location, + qr_code, status), `document_movement` (doc_id, from, to, by, at, note), + `document_attachment` (file via StorageProvider). + + +--- + +## Pillar 3 — AI-Queryable Data Layer + +### Goal +Every module exposes a **consistent, queryable read surface** so that: +- **CEO conversational AI** ("what's happening?") can answer across all modules. +- **AI insights** (delays, good/poor performance, bottlenecks) auto-surface. +- **ManCom consolidation** pulls from every department uniformly. + +### Design rules +- Each module ships a **read model / query endpoint** designed for agent + consumption (stable shapes, documented, RBAC-aware). +- Off-box agents (HCI/workstations) are the consumers — they call the API, do + the reasoning off-box, and post results back. +- The **agentic ManCom iframe** and **CEO chat** are clients of this layer. + +### Agentic ManCom iframe (host contract) +- The CEO "agentic view" is a **sandboxed iframe** whose **HTML is generated + externally by an AI agent** and auto-loaded ("statically dynamic"). +- Host shell defines: how the agent HTML is fetched/refreshed, which data + endpoints the iframe may read, and a strict sandbox (no arbitrary network, + RBAC-scoped data only). Maximum design flexibility, safely contained. + + +--- + +## Engineering Module + +Covers: Bidding, Financial Planning, Quantity, Billing, Contracts, Archiving. +The technical office and the heart of the contractor. Defines projects + pay +items that the whole suite references. + +### Project lifecycle +1. **Bidding** — projects usually known prior to bidding (sourcing from DPWH + site is a later AI-agent nicety, not core). Prepare bid → DPWH process → NTP. +2. **Financial Planning** — after NTP: + - **Office budget** (Engineering) and **field budget** (PE on site). + - Variance = **savings / deleted items**, tracked **per pay item**. +3. **Execution** — QE requests materials (see Materials flow), weekly SWA, + weekly progress photos. +4. **Billing** — monthly billing to DPWH. + +### Materials flow (CEO priority) +- **PE prepares the weekly materials schedule.** UI must be **dead-simple for + older engineers** and **AI-assisted** (AI pre-fills from program of work + + remaining quantities; PE adjusts). +- Approval chain: **PE → PM (approve) → VPO**. +- Approved schedule → **Procurement**. +- Separate **weekly equipment schedule** → submitted to **Omega (OAEC via API)**. + +### SWA & progress +- QE submits **weekly SWA / measurements**, consolidated for reports. +- QE captures **weekly geotagged progress photos** (phone, edge, <500 KB) — these + are **reused by Billing**. + +### Billing +- **Billing keeps its own SWA** (may reference/suggest from QE SWA but is + distinct). +- **Billing cycle is monthly** (QE SWA is weekly) — reconcile internally; the + DPWH report/billing is its own artifact, separate from the weekly internal SWA. +- **Photos are the real anchor** for billing; QE progress photos feed billing + photos. Billing also pulls MQC certs (see MQC). + +### Pay items +- **Master DPWH Blue Book pay-item library** (shared reference). Quantity, + Billing, MQC, SWA, Survey all reference item numbers. + +### Acumatica boundary +- PO, expenses, payment requests live in **Acumatica**. Engineering produces the + requests/quantities; financial truth stays in Acumatica. + +### Archiving +- Project document archive; integrates with Document Tracking (Pillar 2) and + Records physical-location index. + + +--- + +## Procurement Module + +### Role +- Receives **approved weekly materials schedules** from Engineering (via + Operations/VPO approval). +- **Monitors supplier delivery performance / efficiency** from receiving data + (see receiving flow below). +- PO/expenses themselves live in **Acumatica**. + +### Receiving (edge, AI-encoded — OAEC/ZeroType pattern) +- **Materials inspectors at site** capture **DR photo + details** on phone + (edge, GPS + timestamp, <500 KB). +- **AI agent (off-box) encodes the capture into Acumatica** — same concept as + OAEC fuel: photo → AI encode → sync. GPS + time stamped. +- Procurement reads this stream to score **delivery performance** per supplier. +- **Cement deliveries** flagged for monitoring; assign an explicit **receiver + role** (TBD with UBI) — currently unclear who receives. + +### Data +- `materials_schedule`, `delivery_receipt` (photo, supplier, items, gps, ts, + acumatica_ref), `supplier_performance` (derived). + + +--- + +## Operations Module — VPO Control Tower + +Operations = the **VPO's office**, the head-office counterpart where field ops +connects up. NOT a data-entry module — an **approvals inbox + project-health +dashboard + subcontract registry**, sitting between field modules and the CEO. + +### Responsibilities +- **Approvals concentrate here**: materials schedules, subcontracts, etc. +- **Subcontract management** (registry + contracts). +- **Procurement follow-ups** — CC'd on everything procurement. +- **Standards-setting**; **document + project performance monitoring**. +- **First-stop rectification** of issues before anything reaches the CEO. Most + reports to the CEO pass through here first. + +### Build as +- Approvals engine client + project health dashboard (consumes Engineering, + Procurement, Survey, MQC read models) + subcon registry. + + +--- + +## Survey Module + +Separate department from MQC. + +### Deliverables (all in scope) +- **Original ground**, **stakeout**, **cross-sections for quantities**, + **as-built**. +- Indexing by **stations / chainage** for road construction (STA 0+000 format). +- **Monthly volume for the ManCom report** — Survey volumes feed ManCom + consolidation. + +### Notes +- Capture + document/data, tagged by station for indexing. +- Cross-section quantities can inform SWA/billing references (pay-item linked). + + +--- + +## MQC Module — Materials Quality Control + +Separate department from Survey. Follows **DPWH minimum testing requirements**. + +### Scope +- **DPWH minimum testing requirements per pay item**, with **lab results**. +- Encode testing rules per item (e.g., quantity of Item 311 → required number of + concrete beam tests). +- **Pouring logs** (standardize the form). +- **Issues materials certs** that **attach to billings** (feed Engineering + Billing). + +### Data +- `qc_test` (project, pay_item, type, result, lab_ref, cert_id), + `pour_log`, `material_cert` (attached to billing via Document Tracking). + + +--- + +## Audit Module + +### Scope (all in) +- **Findings/observations register**, **follow-up tracking**, **compliance + checklists**. +- **Oversight read-access** across modules. + +### Exception register (Investigation Tracker) +- Items that **fail a check but need deeper investigation** are raised as + **exceptions/flags**: assigned → investigated → resolved or escalated, with a + **full audit trail**. + +### Data +- `audit_finding`, `audit_followup`, `compliance_checklist`, + `audit_exception` (status: raised/investigating/resolved/escalated). + + +--- + +## IT Module (Marvin's home turf — expect heavy iteration) + +### 1. Centralized monitoring +- **SNMP** monitoring of devices: APC/Ablerex UPS, H3C core switches, Hikvision + NVR, Sangfor (HCI/NGAF/Endpoint). +- **Device map / topology view** (map view of devices). +- **Consolidated firewall + endpoint status** from Sangfor NGAF/Endpoint. + +### 2. DeskGuard v2 (activity agent — evolved) +- Old version: logged window-title switches only (screenshot as support). +- **New version philosophy shift:** from pure backend surveillance → + **self-reporting productivity tool** (Time Doctor / oDesk style). +- Agent captures: **OCR of screen text (CLIENT-SIDE)** + window title + + **keystroke/mouse-stroke counts** → POST small JSON to API. +- **Screenshots optional, sampled, compressed <200 KB** (text already extracted + client-side, so screenshot is just evidence). +- User picks **activity from a KPI dropdown** + notes. +- **End-of-day personal dashboard:** top apps, hours spent, key activity, + KPI-hours. +- **MANDATORY policy/consent layer:** visible notice that activity capture is + **for official use on company-owned assets only**; staff reminded to avoid + personal activity on company devices. Role-scoped access to captured data. + Flag + notification on this banner. + +### 3. AI Helpdesk +- Ticket **landing + Kanban**. +- **Auto-assign by tech location ↔ ticket location**. +- **Per-tech ticket charts**, target **3 tickets/tech/day**. +- Shares the **Ticketing engine** with Admin inquiries. + +### 4. Maintenance scheduler (PMS) +- Pulls **active properties from Property module**. +- **Auto-generates quarterly PMS** schedules; **auto-closes when done**. + +### 5. Environmental monitoring +- Server-room + ambient **temperature** via **DHT22 + ESP32**, live running + charts. Ingest **SNMP/IPMI temperature where devices expose it**, ESP32 + sensors where not. + +### 6. Capacity Planning & Controls +The IT control room for resource headroom — forecast exhaustion before it bites, +and enforce the architectural scaling triggers from `CLAUDE.md`. + +- **Resource utilization tracking** (trended over time): + - **Storage** — disk usage vs the 320 GB box; **forecast "disk full" date** by + trend (directly governs the photo-storage strategy). + - **Data transfer** — outbound vs the Lightsail 6 TB/month allowance (overage + alarms). + - **Compute/DB** — CPU burst headroom, PostgreSQL growth. + - **UPS load** — actual load vs **rated capacity** (APC/Ablerex), runtime + headroom. + - **Network bandwidth per site**; **license/seat counts**. +- **Forecasting** — extend each trend to its limit and flag the projected + breach date (same trend-projection idea as the ManCom view). +- **Controls / thresholds** — green/amber/red thresholds per resource with + alerts; tracks the **scaling triggers** (e.g., "split Postgres," "move photos + to object storage") as live, owned action items. +- **Change controls** — record capacity-affecting changes (new agents deployed, + new modules enabled) so a utilization jump has a traceable cause. +- Feeds the **CEO/IT visibility** layer. + +### Suggested additions (confirm keep/drop with Marvin) +- IT **asset inventory** (source of truth for company devices + installed + DeskGuard agents; ties to Property). +- **License/subscription/warranty expiry** (→ Expiry Reminder service). +- **Network/internet uptime + bandwidth per site**. +- **Backup status dashboard**. +- **Change/access log** (admin-rights grants, on/off-boarding provisioning). + +> Marvin noted "lots more here — this is mine." Treat IT as the most +> iteratively-expanded module. + + +--- + +## Records Module + +### Scope +- Company-wide **document registry** (incoming/outgoing correspondence, + transmittals, controlled docs) — the home of **Document Tracking (Pillar 2)**. +- **Vehicle registrations & expiry, insurance, document stamps** — with + **expiry alarms/notifications** (→ shared Expiry/Renewal Reminder service). +- **Physical location index**: e.g., "Land Title X is in Steel Cabinet 3, + Drawer B." **AI-searchable but HARD-GATED by user access** — sensitive docs + (titles, etc.) only visible to authorized roles. + +### Data +- `record_doc`, `vehicle_doc` (reg/insurance/stamp + expiry), + `physical_location` (cabinet/drawer index, access-controlled). + + +--- + +## Clinic Module (isolated + encrypted) + +Org-wise sits **under Admin**, but data is strictly isolated. + +### Hard requirements +- **Separate database**, **encrypted at rest** (field-level encryption for + medical data). +- **Strict role isolation** — only clinic staff see individual records. +- **CEO/DOLE visibility is AGGREGATE only** (injury rates, medical-exam + completion, incident counts) — never individual medical detail. + +### Scope (all in) +- Pre-employment / annual **medicals**, **site clinic visits / medicine + issuance**, **injury/incident reports**, **DOLE compliance**. + + +--- + +## Admin / OHS Module — Site Gateway + +Broad hub. The **human bridge connecting HR ↔ site**, plus the **safety/OHS hub**. + +### Sub-areas +- **OHS:** safety trainings, health/awareness seminars, schedules, **toolbox + meeting logs**. +- **Clinic** sits under Admin org-wise (but keeps its isolated encrypted DB). +- **Dispensing**: safety gear (PPE) + medicine. +- **Admin Officers' portal** + **timekeepers** — gateway of HR to site; + inquiries/requests routing (shares **Ticketing engine** with IT helpdesk). +- **Building admin:** conference room **booking/reservations**, facilities. + +### Shared touchpoints (don't rebuild) +- PPE/medicine dispensing overlaps **Property**. +- Timekeeping overlaps **HR**. + + +--- + +## HR Module (thin) + +### Boundary +- **Ezacctg is the system of record** for 201 files, payroll, statutory + (SSS/PhilHealth/Pag-IBIG). Almost complete — **except hours computation**. +- The suite does **NOT** rebuild HR. + +### What the suite adds (later phase) +- **Hours-computation engine**: compute hours/OT/ND from raw attendance logs, + push clean totals to Ezacctg (patches the missing Ezacctg function). + +### Attendance reality (today) +- Tablets at sites: **photo + employee PIN**, syncs to Ezacctg. (Tablets are the + BSmart-adjacent hardware? No — BSmart = fuel dispensers; attendance tablets + are separate.) +- **Phase 2:** centralized **Face ID** on existing tablets (edge-app pattern). + +### DTR +- All under payroll → record DTRs. + + +--- + +## Property Module (QR-based lightweight EAM) + +### Scope (all in) +- Equipment/asset **custody & accountability**: PAR/MR receipts (who holds what), + warehouse materials custody, tool issuance/return at sites. +- UBI's own non-fleet assets (tools, IT equipment, office assets). Omega fleet is + OAEC's domain; cross-reference where leased to UBI. + +### QR mobile tracking +- Property's **label printer generates QR codes**. +- **Mobile app scans QR** to: log **location + status**, **update custodian**. +- **Monthly custodian attestation**: each custodian confirms/updates equipment + issued to them → audit trail of who-held-what over time. + +### Feeds +- Active assets feed **IT PMS scheduler** (quarterly maintenance). + +### Data +- `asset` (qr, type, status, location, custodian), `asset_movement`, + `custodian_attestation` (monthly). + + +--- + +## Finance Module + +### Boundary +- **No new build now** beyond document tracking. Acumatica does GL/AP/AR, PO, + payment requests. + +### What it needs (heavy Document-Tracking consumer) +- **Batching/boxing** documents transported to **cashier**. +- **Receiving incoming docs** from Records and other departments. +- This is a primary reason Document Tracking (Pillar 2) is a backbone service — + Finance lives on "where is this paper now?" + + +--- + +## CEO / Office of the President Module + +The endpoint. Sits over everything. Build last. + +### Main dashboard = ManCom +- **Live ManCom report view** — consolidated from all departments (Survey + monthly volumes, Engineering SWA/billing, MQC, Operations health, etc.). +- **Trend projection toggle (checkbox):** extrapolate current trajectory — + e.g., "this project trends to **negative slippage** next month" — by extending + charts forward on recent trend (accrual/forecast). + +### AI Insights view +- Auto-surfaced: **delayed reports, good/poor performance, bottlenecks**. + +### Conversational AI +- CEO chats **"what's happening?"** → answers across the whole suite (agentic, + reads the AI-Queryable Data Layer / Pillar 3). + +### Agentic AI view (special) +- A page hosting a **sandboxed iframe** whose **HTML is generated externally by + an AI agent** and auto-loads ("statically dynamic" — design changes with the + agent). See `_pillar-ai-layer.md` for the host contract. Detailed design is a + separate future session. + +### Access +- CEO/VPO: cross-cutting visibility. Clinic data only ever **aggregate** here. + diff --git a/GIT.md b/GIT.md new file mode 100644 index 0000000..ac618c5 --- /dev/null +++ b/GIT.md @@ -0,0 +1,34 @@ +# Git & Branching Strategy — Jules White style + +> Full rationale lives in `UBI-Construction-Suite-CONTEXT.md` §11. This is the +> quick reference. + +## Layers +- `main` — protected, always-releasable. PR-only. +- **module branch** — one per module/pillar: `engineering`, `it`, `pillar-auth`, … +- **feature branch** — `__` + +## Feature branch naming +`__` (underscores between parts, hyphens inside the name) + +Examples: +- `engineering_billing_monthly-dpwh` +- `procurement_receiving_dr-ai-encode` +- `it_capacity_storage-forecast` +- `property_qr_custodian-attestation` + +## Flow +feature → PR → module branch → PR → `main` + +## Commits (conventional, scoped) +`feat(engineering/billing): ...` · `fix(it/capacity): ...` · `chore(pillar-auth): ...` + +## Rules +- One Claude Code session = one feature branch, kept small. +- No secrets, no binaries in git (`uploads/`, `.env*` ignored). +- Tag releases on `main`: `v0.1.0-pillars`, `v0.2.0-engineering`, … + +## Module / pillar branches +pillar-auth · pillar-doc-tracking · pillar-ai-layer · engineering · procurement · +operations · survey · mqc · audit · it · records · clinic · admin · hr · property · +finance · ceo diff --git a/README.md b/README.md index f13665c..0d327ba 100644 --- a/README.md +++ b/README.md @@ -1 +1,50 @@ -# claude-code \ No newline at end of file +# UBI Construction Suite + +Full construction-management suite for **Ulticon Builders, Inc.** and **Omega Asia**, +covering everything outside the Acumatica ERP. This repo currently holds the +**kickoff context** for Claude Code to build from. + +## Contents +- **`CLAUDE.md`** — the single kickoff context (auto-loaded by Claude Code). + Part I: stack, hard rules, three pillars, shared services, conventions, + module index, phasing, capacity, **and Git strategy (§11)**. + Part II: the three pillars in full + all 14 module specs. +- **`GIT.md`** — branching quick reference (Jules White style). +- **`diagrams.html`** — functional & data-flow diagrams (open in a browser; + Print → Save as PDF for a PDF copy). +- **`mockups.html`** — annotated UI mockups for the CEO / Office of the + President module: ManCom dashboard, trend-projection toggle, AI Insights, + Ask AI, Agentic-view host shell, mobile ManCom, and the + widget→read-model contract table. Open in a browser; Print → PDF to share. +- **`.gitignore`** — Node/TS monorepo ignores; keeps secrets/binaries out. + +## Branching (see `GIT.md` / `CLAUDE.md` §11) +`main` ← module branch (`engineering`, `it`, `pillar-auth`, …) ← feature branch +`__`. + +## Status +Breadth-first build is live: monorepo (NestJS API + React PWA + Postgres + +StorageProvider), the three pillars working (auth/RBAC, doc tracking, +AI-queryable read-model registry), shared engines (approvals, ticketing, +notifications, expiry), Engineering core (pay-item library, AI-prefilled +weekly materials schedule → PM → VPO chain), CEO ManCom dashboard wired +end-to-end, and read-surface stubs for every other department. Refinement +happens per module branch. + +## Run locally +```bash +cp .env.example .env # endpoints externalized — hard rule 6 +docker compose -f docker-compose.dev.yml up -d # postgres + redis (or use your own) +npm install +npm run dev:api # NestJS on :3000 (seeds demo data on first boot) +npm run dev:web # Vite PWA on :5173 (proxies /api) +``` +Demo logins: `ceo@ubi.ph/ceo123` · `vpo@ubi.ph/vpo123` · `pm@ubi.ph/pm123` +· `pe@ubi.ph/pe123` · `admin@ubi.ph/admin123`. + +## Open items to confirm +- Cement-delivery **receiver role**. +- IT module **suggested additions** (asset inventory, license/warranty expiry, + uptime/bandwidth, backup dashboard, change/access log) — keep/drop. +- HR **hours-computation** (OT/ND) rules. +- Agentic AI page — detailed design (separate session). diff --git a/apps/api/nest-cli.json b/apps/api/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/apps/api/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..2ef4083 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,40 @@ +{ + "name": "@ubi/api", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "nest build", + "start": "node dist/main.js", + "start:dev": "nest start --watch", + "typecheck": "tsc -p tsconfig.json --noEmit", + "migration:generate": "typeorm-ts-node-commonjs -d src/data-source.ts migration:generate src/migrations/Migration", + "migration:run": "typeorm-ts-node-commonjs -d src/data-source.ts migration:run", + "migration:revert": "typeorm-ts-node-commonjs -d src/data-source.ts migration:revert" + }, + "dependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/config": "^4.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/jwt": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/typeorm": "^11.0.0", + "@ubi/storage": "*", + "@ubi/types": "*", + "bcryptjs": "^2.4.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "helmet": "^8.0.0", + "pg": "^8.13.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "typeorm": "^0.3.20" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@types/bcryptjs": "^2.4.6", + "@types/express": "^5.0.0", + "@types/multer": "^1.4.12", + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + } +} diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts new file mode 100644 index 0000000..cf1f2b7 --- /dev/null +++ b/apps/api/src/app.module.ts @@ -0,0 +1,80 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { APP_GUARD } from '@nestjs/core'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RbacGuard } from './common/rbac'; +import { HealthController } from './health/health.controller'; +import { CeoModule } from './modules/ceo/ceo.module'; +import { DeptModule } from './modules/dept/dept.module'; +import { EngineeringModule } from './modules/engineering/engineering.module'; +import { EquipmentModule } from './modules/equipment/equipment.module'; +import { ItModule } from './modules/it/it.module'; +import { MqcModule } from './modules/mqc/mqc.module'; +import { OmegaModule } from './modules/omega/omega.module'; +import { OperationsModule } from './modules/operations/operations.module'; +import { SurveyModule } from './modules/survey/survey.module'; +import { ProcurementModule } from './modules/procurement/procurement.module'; +import { PropertyModule } from './modules/property/property.module'; +import { QuantityModule } from './modules/quantity/quantity.module'; +import { RecordsModule } from './modules/records/records.module'; +import { StubsModule } from './modules/stubs/stubs.module'; +import { AiLayerModule } from './pillars/ai-layer/ai-layer.module'; +import { AuthModule } from './pillars/auth/auth.module'; +import { DocTrackingModule } from './pillars/doc-tracking/doc-tracking.module'; +import { SeedModule } from './seed/seed.module'; +import { ApprovalsModule } from './shared/approvals/approvals.module'; +import { ExpiryModule } from './shared/expiry/expiry.module'; +import { FilesModule } from './shared/files/files.module'; +import { NotificationsModule } from './shared/notifications/notifications.module'; +import { TicketingModule } from './shared/ticketing/ticketing.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ isGlobal: true, envFilePath: ['.env', '../../.env'] }), + // Stateful endpoints are EXTERNAL from line one (hard rule 6) — even + // co-located, the API only ever knows host:port from config. + TypeOrmModule.forRootAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + type: 'postgres' as const, + host: config.get('DB_HOST', '127.0.0.1'), + port: Number(config.get('DB_PORT', 5432)), + username: config.get('DB_USER', 'ubi'), + password: config.get('DB_PASS', 'ubi_dev'), + database: config.get('DB_NAME', 'ubi_suite'), + autoLoadEntities: true, + // Dev convenience; migrations replace this before production. + synchronize: config.get('DB_SYNC') === 'true', + }), + }), + // Pillars — built before modules, used by all. + AuthModule, + AiLayerModule, + DocTrackingModule, + // Shared services — build once, reuse everywhere. + FilesModule, + ApprovalsModule, + TicketingModule, + NotificationsModule, + ExpiryModule, + // Department modules (breadth-first; deep work on module branches). + EngineeringModule, + QuantityModule, + EquipmentModule, + ProcurementModule, + OperationsModule, + PropertyModule, + RecordsModule, + ItModule, + OmegaModule, + SurveyModule, + MqcModule, + DeptModule, + StubsModule, + CeoModule, + SeedModule, + ], + controllers: [HealthController], + providers: [{ provide: APP_GUARD, useClass: RbacGuard }], +}) +export class AppModule {} diff --git a/apps/api/src/common/base.entity.ts b/apps/api/src/common/base.entity.ts new file mode 100644 index 0000000..0f8835e --- /dev/null +++ b/apps/api/src/common/base.entity.ts @@ -0,0 +1,32 @@ +import type { BusinessEntity } from '@ubi/types'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +/** + * Suite-wide conventions (§7): every table is entity-scoped and carries + * created_by / created_at / updated_at, soft-delete where audit matters. + */ +export abstract class BaseAppEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'entity_id', type: 'varchar', length: 8, default: 'UBI' }) + entity: BusinessEntity; + + @Column({ name: 'created_by', type: 'varchar', nullable: true }) + createdBy: string | null; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; + + @DeleteDateColumn({ name: 'deleted_at', nullable: true }) + deletedAt: Date | null; +} diff --git a/apps/api/src/common/rbac.ts b/apps/api/src/common/rbac.ts new file mode 100644 index 0000000..bed8802 --- /dev/null +++ b/apps/api/src/common/rbac.ts @@ -0,0 +1,64 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + SetMetadata, + UnauthorizedException, + ForbiddenException, + createParamDecorator, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { JwtService } from '@nestjs/jwt'; +import type { UserClaims } from '@ubi/types'; +import type { Request } from 'express'; + +export const IS_PUBLIC = 'isPublic'; +/** Route requires no authentication (login, health). */ +export const Public = () => SetMetadata(IS_PUBLIC, true); + +export const REQUIRED_CLAIMS = 'requiredClaims'; +/** Caller must hold at least one of the listed claims (any-of). */ +export const RequireClaims = (...claims: string[]) => SetMetadata(REQUIRED_CLAIMS, claims); + +export const CurrentUser = createParamDecorator((_data: unknown, ctx: ExecutionContext): UserClaims => { + const req = ctx.switchToHttp().getRequest(); + if (!req.user) throw new UnauthorizedException(); + return req.user; +}); + +/** Cross-cutting reads (CEO/VPO) are expressed as a claim, not a bypass. */ +export function hasClaim(user: UserClaims, claim: string): boolean { + return user.claims.includes(claim) || user.claims.includes('role:admin'); +} + +@Injectable() +export class RbacGuard implements CanActivate { + constructor( + private readonly reflector: Reflector, + private readonly jwt: JwtService, + ) {} + + async canActivate(ctx: ExecutionContext): Promise { + const targets = [ctx.getHandler(), ctx.getClass()]; + if (this.reflector.getAllAndOverride(IS_PUBLIC, targets)) return true; + + const req = ctx.switchToHttp().getRequest(); + const header = req.headers.authorization ?? ''; + const token = header.startsWith('Bearer ') ? header.slice(7) : null; + if (!token) throw new UnauthorizedException('missing bearer token'); + + let user: UserClaims; + try { + user = await this.jwt.verifyAsync(token); + } catch { + throw new UnauthorizedException('invalid or expired token'); + } + req.user = user; + + const required = this.reflector.getAllAndOverride(REQUIRED_CLAIMS, targets); + if (required?.length && !required.some((c) => hasClaim(user, c))) { + throw new ForbiddenException(`requires one of: ${required.join(', ')}`); + } + return true; + } +} diff --git a/apps/api/src/data-source.ts b/apps/api/src/data-source.ts new file mode 100644 index 0000000..430d305 --- /dev/null +++ b/apps/api/src/data-source.ts @@ -0,0 +1,23 @@ +import 'reflect-metadata'; +import * as dotenv from 'dotenv'; +import { DataSource } from 'typeorm'; + +dotenv.config({ path: '../../.env' }); +dotenv.config({ path: '.env' }); + +/** + * TypeORM CLI data-source — used only by migration:generate / migration:run. + * The NestJS app uses TypeOrmModule.forRootAsync in app.module.ts. + * Set DB_SYNC=false in production and run migrations instead. + */ +export const AppDataSource = new DataSource({ + type: 'postgres', + host: process.env.DB_HOST ?? '127.0.0.1', + port: Number(process.env.DB_PORT ?? 5432), + username: process.env.DB_USER ?? 'ubi', + password: process.env.DB_PASS ?? 'ubi_dev', + database: process.env.DB_NAME ?? 'ubi_suite', + entities: ['src/**/*.entity.ts', 'src/modules/**/*.module.ts', 'src/modules/**/*.entity.ts'], + migrations: ['src/migrations/*.ts'], + synchronize: false, +}); diff --git a/apps/api/src/health/health.controller.ts b/apps/api/src/health/health.controller.ts new file mode 100644 index 0000000..89b4255 --- /dev/null +++ b/apps/api/src/health/health.controller.ts @@ -0,0 +1,11 @@ +import { Controller, Get } from '@nestjs/common'; +import { Public } from '../common/rbac'; + +@Controller('health') +export class HealthController { + @Public() + @Get() + health(): { status: string; service: string; at: string } { + return { status: 'ok', service: 'ubi-suite-api', at: new Date().toISOString() }; + } +} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts new file mode 100644 index 0000000..696510b --- /dev/null +++ b/apps/api/src/main.ts @@ -0,0 +1,21 @@ +import 'reflect-metadata'; +import { ValidationPipe } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import helmet from 'helmet'; +import { AppModule } from './app.module'; + +async function bootstrap(): Promise { + const app = await NestFactory.create(AppModule); + app.use(helmet()); + app.setGlobalPrefix('api'); + app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); + // Pin CORS in production: CORS_ORIGIN=https://edge.ubi-as.com[,more] + const corsOrigin = process.env.CORS_ORIGIN?.split(',').map((o) => o.trim()); + app.enableCors({ origin: corsOrigin?.length ? corsOrigin : true, credentials: true }); + const port = Number(process.env.PORT ?? 3000); + await app.listen(port); + // eslint-disable-next-line no-console + console.log(`UBI Suite API listening on :${port}`); +} + +void bootstrap(); diff --git a/apps/api/src/modules/ceo/ceo.module.ts b/apps/api/src/modules/ceo/ceo.module.ts new file mode 100644 index 0000000..ec25353 --- /dev/null +++ b/apps/api/src/modules/ceo/ceo.module.ts @@ -0,0 +1,126 @@ +import { Controller, Get, Injectable, Module } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { FlaggedProject, Insight, ManComSnapshot, TrendPoint, UserClaims } from '@ubi/types'; +import { Repository } from 'typeorm'; +import { CurrentUser, RequireClaims } from '../../common/rbac'; +import { InsightEntity } from '../../pillars/ai-layer/insight.entity'; +import { ReadModelRegistry } from '../../pillars/ai-layer/read-model.registry'; +import { ApprovalsService } from '../../shared/approvals/approvals.service'; +import { ProjectEntity } from '../engineering/entities'; +import { EngineeringModule } from '../engineering/engineering.module'; +import { QuantityModule, QuantityService } from '../quantity/quantity.module'; +import { seedReportFeed } from '../stubs/stubs.module'; + +/** + * CEO / Office of the President — the endpoint. Consolidates every + * department through the Pillar-3 registry; ships last, but its read-model + * demands are designed in from day one. + */ +@Injectable() +export class CeoService { + constructor( + private readonly registry: ReadModelRegistry, + private readonly approvals: ApprovalsService, + private readonly quantity: QuantityService, + @InjectRepository(ProjectEntity) private readonly projects: Repository, + @InjectRepository(InsightEntity) private readonly insights: Repository, + ) {} + + private async fromRegistry(user: UserClaims, key: string, fallback: T): Promise { + const model = this.registry.get(key); + if (!model) return fallback; + return (await model.handler(user)) as T; + } + + private toInsight(row: InsightEntity): Insight { + return { + id: row.id, + severity: row.severity, + title: row.title, + body: row.body, + evidence: row.evidence, + actions: row.actions, + generatedAt: row.createdAt.toISOString(), + generatedBy: row.generatedBy, + }; + } + + async snapshot(user: UserClaims): Promise { + const projects = await this.projects.find({ where: { status: 'active' } }); + const flagged = await this.fromRegistry(user, 'operations.health', []); + const exceptions = await this.fromRegistry>(user, 'audit.exceptions', []); + const accomplishment = await this.fromRegistry(user, 'engineering.swa', []); + const surveyVolumes = await this.fromRegistry(user, 'survey.volumes', []); + const topInsights = ( + await this.insights.find({ where: { dismissed: false }, order: { createdAt: 'DESC' }, take: 3 }) + ).map((r) => this.toInsight(r)); + + const avgSlippage = projects.length + ? projects.reduce((sum, p) => sum + p.slippagePct, 0) / projects.length + : 0; + + // Real accomplishment from the Quantity weekly rollup (QE field entries). + const qhead = await this.quantity.headSummary(); + const quantityTop = qhead.rows + .slice() + .sort((a, b) => b.thisMonth - a.thisMonth) + .slice(0, 5) + .map((r) => ({ code: r.code, name: r.name, pe: r.pe, pct: r.pct, thisMonth: r.thisMonth })); + + return { + asOf: new Date().toISOString(), + kpis: { + activeProjects: projects.length, + flaggedProjects: flagged.filter((f) => f.slippagePct < 0).length, + billingsMonthCentavos: 18_240_000_000, + billingsDeltaPct: 12, + billingDraftsSubmitted: 21, + billingDraftsTotal: 34, + avgSlippagePct: Number(avgSlippage.toFixed(2)), + projectedSlippagePct: Number((avgSlippage - 0.6).toFixed(2)), + openExceptions: exceptions.length, + escalatedExceptions: exceptions.filter((e) => e.status === 'escalated').length, + accomplishmentPct: Number(qhead.overallPct.toFixed(4)), + accomplishmentMonth: Math.round(qhead.thisMonth), + }, + accomplishment, + surveyVolumes, + flagged, + reportFeed: seedReportFeed(), + topInsights, + quantityTop, + }; + } + + async allInsights(): Promise { + const rows = await this.insights.find({ + where: { dismissed: false }, + order: { createdAt: 'DESC' }, + take: 50, + }); + return rows.map((r) => this.toInsight(r)); + } +} + +@Controller('ceo') +@RequireClaims('role:ceo', 'role:vpo') +export class CeoController { + constructor(private readonly svc: CeoService) {} + + @Get('mancom') + mancom(@CurrentUser() user: UserClaims): Promise { + return this.svc.snapshot(user); + } + + @Get('insights') + insights(): Promise { + return this.svc.allInsights(); + } +} + +@Module({ + imports: [EngineeringModule, QuantityModule], + controllers: [CeoController], + providers: [CeoService], +}) +export class CeoModule {} diff --git a/apps/api/src/modules/dept/dept.module.ts b/apps/api/src/modules/dept/dept.module.ts new file mode 100644 index 0000000..899f036 --- /dev/null +++ b/apps/api/src/modules/dept/dept.module.ts @@ -0,0 +1,157 @@ +import { Controller, ForbiddenException, Get, Injectable, Module, NotFoundException, Param } from '@nestjs/common'; +import type { UserClaims } from '@ubi/types'; +import { CurrentUser, hasClaim } from '../../common/rbac'; +import { ReadModelRegistry } from '../../pillars/ai-layer/read-model.registry'; +import { ApprovalsService } from '../../shared/approvals/approvals.service'; +import { seedReportFeed } from '../stubs/stubs.module'; + +/** One definition per department — head account, the read-model it monitors, + * and its CCTV camera id. Standard dashboard shape is derived from this. */ +export interface DeptDef { + slug: string; + label: string; + /** Pillar-3 read-model key that holds "what this dept monitors". */ + readModelKey: string; + monitoringTitle: string; + cameraId: string; + headEmail: string; + headName: string; +} + +export const DEPARTMENTS: DeptDef[] = [ + { slug: 'engineering', label: 'Engineering', readModelKey: 'engineering.projects', monitoringTitle: 'Projects & accomplishment', cameraId: 'CAM-ENG-01', headEmail: 'head-engineering@ubi.ph', headName: 'Engr. A. Salonga' }, + { slug: 'procurement', label: 'Procurement', readModelKey: 'procurement.supplier-performance', monitoringTitle: 'Supplier delivery performance', cameraId: 'CAM-PRC-01', headEmail: 'head-procurement@ubi.ph', headName: 'M. Tan' }, + { slug: 'operations', label: 'Operations (VPO)', readModelKey: 'operations.health', monitoringTitle: 'Project health & flags', cameraId: 'CAM-OPS-01', headEmail: 'head-operations@ubi.ph', headName: 'VPO Office' }, + { slug: 'survey', label: 'Survey', readModelKey: 'survey.volumes', monitoringTitle: 'Monthly volumes by station', cameraId: 'CAM-SUR-01', headEmail: 'head-survey@ubi.ph', headName: 'Engr. L. Reyes' }, + { slug: 'mqc', label: 'MQC', readModelKey: 'mqc.certs', monitoringTitle: 'Test & certificate queue', cameraId: 'CAM-MQC-01', headEmail: 'head-mqc@ubi.ph', headName: 'Engr. P. Cruz' }, + { slug: 'audit', label: 'Audit', readModelKey: 'audit.exceptions', monitoringTitle: 'Exception / investigation register', cameraId: 'CAM-AUD-01', headEmail: 'head-audit@ubi.ph', headName: 'C. Domingo' }, + { slug: 'it', label: 'IT', readModelKey: 'it.capacity', monitoringTitle: 'Capacity, devices & helpdesk', cameraId: 'CAM-IT-01', headEmail: 'head-it@ubi.ph', headName: 'Marvin (IT Head)' }, + { slug: 'records', label: 'Records', readModelKey: 'records.documents', monitoringTitle: 'Document registry & movement', cameraId: 'CAM-REC-01', headEmail: 'head-records@ubi.ph', headName: 'R. Bautista' }, + { slug: 'clinic', label: 'Clinic', readModelKey: 'clinic.aggregate', monitoringTitle: 'Aggregate health (isolated)', cameraId: 'CAM-CLN-01', headEmail: 'head-clinic@ubi.ph', headName: 'Dr. S. Lim' }, + { slug: 'admin', label: 'Admin / OHS', readModelKey: 'admin.tickets', monitoringTitle: 'Inquiries, OHS & dispensing', cameraId: 'CAM-ADM-01', headEmail: 'head-admin@ubi.ph', headName: 'G. Flores' }, + { slug: 'hr', label: 'HR', readModelKey: 'hr.hours', monitoringTitle: 'Hours computation runs', cameraId: 'CAM-HR-01', headEmail: 'head-hr@ubi.ph', headName: 'J. Aquino' }, + { slug: 'property', label: 'Property', readModelKey: 'property.assets', monitoringTitle: 'Asset custody (QR EAM)', cameraId: 'CAM-PRP-01', headEmail: 'head-property@ubi.ph', headName: 'D. Ong' }, + { slug: 'finance', label: 'Finance', readModelKey: 'finance.doc-flow', monitoringTitle: 'Documents in motion', cameraId: 'CAM-FIN-01', headEmail: 'head-finance@ubi.ph', headName: 'V. Santos' }, +]; + +export interface StandardKpi { + label: string; + value: string; + sub: string; + tone: 'ok' | 'warn' | 'bad' | 'info'; +} + +export interface DeptDashboard { + slug: string; + label: string; + head: { email: string; name: string }; + /** STANDARD across every department — same four cards everywhere. */ + kpis: StandardKpi[]; + monitoring: { title: string; src: string; data: unknown }; + /** CCTV thumbnail meta. online=false until an NVR/stream is wired (IT spec). */ + camera: { id: string; label: string; online: boolean; streamUrl: string | null }; +} + +@Injectable() +export class DeptService { + constructor( + private readonly registry: ReadModelRegistry, + private readonly approvals: ApprovalsService, + ) {} + + list(): DeptDef[] { + return DEPARTMENTS; + } + + private countAttention(slug: string, data: unknown): { n: number; note: string; tone: StandardKpi['tone'] } { + const rows = Array.isArray(data) ? (data as Array>) : []; + if (slug === 'it') { + const n = rows.filter((r) => r.status === 'amber' || r.status === 'red').length; + return { n, note: 'resources amber/red', tone: n ? 'warn' : 'ok' }; + } + if (slug === 'audit') { + const n = rows.filter((r) => r.status === 'escalated').length; + return { n, note: 'escalated exceptions', tone: n ? 'bad' : 'ok' }; + } + if (slug === 'mqc') { + const n = rows.filter((r) => r.status === 'pending').length; + return { n, note: 'certs pending', tone: n ? 'warn' : 'ok' }; + } + if (slug === 'records') { + return { n: rows.length, note: 'expiring ≤30 days', tone: rows.length ? 'warn' : 'ok' }; + } + return { n: 0, note: 'all clear', tone: 'ok' }; + } + + async dashboard(user: UserClaims, slug: string): Promise { + const def = DEPARTMENTS.find((d) => d.slug === slug); + if (!def) throw new NotFoundException(`unknown department: ${slug}`); + + // RBAC: that dept's head/staff, or cross-cutting exec/admin. + const allowed = + hasClaim(user, 'role:ceo') || + hasClaim(user, 'role:vpo') || + hasClaim(user, 'role:admin') || + hasClaim(user, `dept:${slug}`); + if (!allowed) throw new ForbiddenException(`requires dept:${slug} or executive scope`); + + const model = this.registry.get(def.readModelKey); + const data = model ? await model.handler(user) : null; + const monitored = Array.isArray(data) ? data.length : data && typeof data === 'object' ? Object.keys(data).length : 0; + + const feed = seedReportFeed().filter((r) => r.module === slug); + const onTime = feed.filter((r) => r.status === 'on-time' || r.status === 'received').length; + const reportTone: StandardKpi['tone'] = feed.some((r) => r.status === 'late') + ? 'bad' + : feed.some((r) => r.status === 'pending') + ? 'warn' + : 'ok'; + + const pendingApprovals = await this.approvals.countPending(); + const attention = this.countAttention(slug, data); + + const kpis: StandardKpi[] = [ + { label: 'Monitored items', value: String(monitored), sub: def.monitoringTitle, tone: 'info' }, + { label: 'Reporting', value: feed.length ? `${onTime}/${feed.length}` : '—', sub: 'on-time this period', tone: reportTone }, + { label: 'Open approvals', value: String(pendingApprovals), sub: 'in shared engine', tone: pendingApprovals ? 'warn' : 'ok' }, + { label: 'Needs attention', value: String(attention.n), sub: attention.note, tone: attention.tone }, + ]; + + const base = process.env.CCTV_BASE_URL?.replace(/\/$/, ''); + return { + slug: def.slug, + label: def.label, + head: { email: def.headEmail, name: def.headName }, + kpis, + monitoring: { title: def.monitoringTitle, src: def.readModelKey, data }, + camera: { + id: def.cameraId, + label: `${def.label} · ${def.cameraId}`, + online: Boolean(base), + streamUrl: base ? `${base}/${def.cameraId}/snapshot.jpg` : null, + }, + }; + } +} + +@Controller('dept') +export class DeptController { + constructor(private readonly svc: DeptService) {} + + @Get() + list(): DeptDef[] { + return this.svc.list(); + } + + @Get(':slug/dashboard') + dashboard(@CurrentUser() user: UserClaims, @Param('slug') slug: string): Promise { + return this.svc.dashboard(user, slug); + } +} + +@Module({ + controllers: [DeptController], + providers: [DeptService], + exports: [DeptService], +}) +export class DeptModule {} diff --git a/apps/api/src/modules/engineering/engineering.controller.ts b/apps/api/src/modules/engineering/engineering.controller.ts new file mode 100644 index 0000000..c971569 --- /dev/null +++ b/apps/api/src/modules/engineering/engineering.controller.ts @@ -0,0 +1,50 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import type { MaterialsScheduleLine, UserClaims } from '@ubi/types'; +import { IsArray, IsDateString, IsString } from 'class-validator'; +import { CurrentUser } from '../../common/rbac'; +import { MaterialsScheduleEntity, PayItemEntity, ProjectEntity } from './entities'; +import { EngineeringService } from './engineering.service'; + +class CreateScheduleDto { + @IsString() projectId: string; + @IsDateString() weekOf: string; + @IsArray() lines: MaterialsScheduleLine[]; +} + +@Controller('engineering') +export class EngineeringController { + constructor(private readonly svc: EngineeringService) {} + + @Get('projects') + projects(): Promise { + return this.svc.listProjects(); + } + + @Get('pay-items') + payItems(): Promise { + return this.svc.listPayItems(); + } + + @Get('materials-schedules') + schedules(): Promise { + return this.svc.listSchedules(); + } + + @Get('materials-schedules/prefill/:projectId') + prefill(@Param('projectId') projectId: string): Promise { + return this.svc.aiPrefill(projectId); + } + + @Post('materials-schedules') + create( + @CurrentUser() user: UserClaims, + @Body() dto: CreateScheduleDto, + ): Promise { + return this.svc.createSchedule(user, dto); + } + + @Post('materials-schedules/:id/submit') + submit(@CurrentUser() user: UserClaims, @Param('id') id: string): Promise { + return this.svc.submitSchedule(user, id); + } +} diff --git a/apps/api/src/modules/engineering/engineering.module.ts b/apps/api/src/modules/engineering/engineering.module.ts new file mode 100644 index 0000000..9addeb8 --- /dev/null +++ b/apps/api/src/modules/engineering/engineering.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { MaterialsScheduleEntity, PayItemEntity, ProjectEntity } from './entities'; +import { EngineeringController } from './engineering.controller'; +import { EngineeringService } from './engineering.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([ProjectEntity, PayItemEntity, MaterialsScheduleEntity])], + controllers: [EngineeringController], + providers: [EngineeringService], + exports: [EngineeringService, TypeOrmModule], +}) +export class EngineeringModule {} diff --git a/apps/api/src/modules/engineering/engineering.service.ts b/apps/api/src/modules/engineering/engineering.service.ts new file mode 100644 index 0000000..31b4617 --- /dev/null +++ b/apps/api/src/modules/engineering/engineering.service.ts @@ -0,0 +1,113 @@ +import { Injectable, NotFoundException, OnModuleInit } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { MaterialsScheduleLine, TrendPoint, UserClaims } from '@ubi/types'; +import { Repository } from 'typeorm'; +import { ReadModelRegistry } from '../../pillars/ai-layer/read-model.registry'; +import { ApprovalsService } from '../../shared/approvals/approvals.service'; +import { MaterialsScheduleEntity, PayItemEntity, ProjectEntity } from './entities'; + +@Injectable() +export class EngineeringService implements OnModuleInit { + constructor( + @InjectRepository(ProjectEntity) private readonly projects: Repository, + @InjectRepository(PayItemEntity) private readonly payItems: Repository, + @InjectRepository(MaterialsScheduleEntity) + private readonly schedules: Repository, + private readonly approvals: ApprovalsService, + private readonly registry: ReadModelRegistry, + ) {} + + onModuleInit(): void { + this.registry.register( + { + key: 'engineering.swa', + module: 'engineering', + title: 'Accomplishment vs plan (cumulative ₱M)', + description: 'Weekly SWA consolidated against the program of work; monthly billing draws from this.', + }, + () => this.accomplishmentTrend(), + ); + this.registry.register( + { + key: 'engineering.projects', + module: 'engineering', + title: 'Project register', + description: 'Active DPWH packages with slippage.', + }, + () => this.listProjects(), + ); + } + + /** + * Scaffold trend: derived constants until SWA capture lands. Shape is the + * contract — solid history, null-projected tail filled by the caller. + */ + accomplishmentTrend(): TrendPoint[] { + const plan = [120, 265, 430, 610, 800, 980, 1130, 1290]; + const actual = [112, 248, 401, 575, 742, 905, null, null]; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug']; + return months.map((period, i) => ({ + period, + plan: plan[i], + actual: actual[i], + projected: actual[i] === null ? 905 + (i - 5) * 155 : null, + })); + } + + listProjects(): Promise { + return this.projects.find({ order: { code: 'ASC' } }); + } + + listPayItems(): Promise { + return this.payItems.find({ order: { itemNo: 'ASC' } }); + } + + /** AI-assisted prefill: stub from remaining program quantities (off-box AI later). */ + async aiPrefill(projectId: string): Promise { + const project = await this.projects.findOne({ where: { id: projectId } }); + if (!project) throw new NotFoundException('no such project'); + return [ + { payItemNo: '311(1)c', material: 'Cement (Type 1)', quantity: 1240, unit: 'bags', aiSuggested: true }, + { payItemNo: '200', material: 'Aggregate subbase course', quantity: 850, unit: 'm³', aiSuggested: true }, + { payItemNo: '404(1)', material: 'RSB 16mm', quantity: 12.4, unit: 't', aiSuggested: true }, + ]; + } + + async createSchedule( + user: UserClaims, + input: { projectId: string; weekOf: string; lines: MaterialsScheduleLine[] }, + ): Promise { + return this.schedules.save( + this.schedules.create({ + projectId: input.projectId, + weekOf: input.weekOf, + lines: input.lines, + status: 'draft', + preparedBy: user.email, + entity: user.entity, + createdBy: user.email, + }), + ); + } + + /** PE submits → PM approves → VPO. Approved schedule flows to Procurement. */ + async submitSchedule(user: UserClaims, id: string): Promise { + const schedule = await this.schedules.findOne({ where: { id } }); + if (!schedule) throw new NotFoundException('no such schedule'); + schedule.status = 'submitted'; + await this.approvals.open(user, { + subjectType: 'materials-schedule', + subjectId: schedule.id, + title: `Weekly materials schedule — wk of ${schedule.weekOf}`, + steps: [ + { step: 'pm', actorClaim: 'role:pm' }, + { step: 'vpo', actorClaim: 'role:vpo' }, + ], + }); + return this.schedules.save(schedule); + } + + listSchedules(): Promise { + return this.schedules.find({ order: { createdAt: 'DESC' }, take: 100 }); + } +} diff --git a/apps/api/src/modules/engineering/entities.ts b/apps/api/src/modules/engineering/entities.ts new file mode 100644 index 0000000..4f0afc9 --- /dev/null +++ b/apps/api/src/modules/engineering/entities.ts @@ -0,0 +1,67 @@ +import type { MaterialsScheduleLine, ScheduleStatus } from '@ubi/types'; +import { Column, Entity, Index } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; + +@Entity('project') +export class ProjectEntity extends BaseAppEntity { + @Index({ unique: true }) + @Column() + code: string; + + @Column() + name: string; + + @Column({ name: 'contract_no' }) + contractNo: string; + + @Column({ type: 'varchar', default: 'active' }) + status: 'bidding' | 'ntp' | 'active' | 'completed'; + + @Column({ name: 'slippage_pct', type: 'real', default: 0 }) + slippagePct: number; + + /** In-house contract amount — denominator for accomplishment %. */ + @Column({ name: 'inhouse_amount', type: 'double precision', default: 0 }) + inhouseAmount: number; + + /** Project engineer / person-in-charge onsite. */ + @Column({ name: 'pe', type: 'varchar', default: '' }) + pe: string; + + /** Accomplishment carried in before the system started (so % + to-date are right). */ + @Column({ name: 'prior_accomplished', type: 'double precision', default: 0 }) + priorAccomplished: number; +} + +/** Master DPWH Blue Book pay-item library — one library, reused everywhere. */ +@Entity('pay_item') +export class PayItemEntity extends BaseAppEntity { + @Index({ unique: true }) + @Column({ name: 'item_no' }) + itemNo: string; + + @Column() + description: string; + + @Column() + unit: string; +} + +@Entity('materials_schedule') +export class MaterialsScheduleEntity extends BaseAppEntity { + @Index() + @Column({ name: 'project_id' }) + projectId: string; + + @Column({ name: 'week_of', type: 'date' }) + weekOf: string; + + @Column({ type: 'jsonb' }) + lines: MaterialsScheduleLine[]; + + @Column({ type: 'varchar', default: 'draft' }) + status: ScheduleStatus; + + @Column({ name: 'prepared_by' }) + preparedBy: string; +} diff --git a/apps/api/src/modules/equipment/equipment.module.ts b/apps/api/src/modules/equipment/equipment.module.ts new file mode 100644 index 0000000..2d8bc67 --- /dev/null +++ b/apps/api/src/modules/equipment/equipment.module.ts @@ -0,0 +1,146 @@ +import { Body, Controller, Get, Injectable, Logger, Module, NotFoundException, OnModuleInit, Param, Post } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import type { UserClaims } from '@ubi/types'; +import { IsArray, IsDateString, IsString, MinLength } from 'class-validator'; +import { Column, Entity, Index, Repository } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; +import { CurrentUser } from '../../common/rbac'; +import { ReadModelRegistry } from '../../pillars/ai-layer/read-model.registry'; + +/** A scheduled activity that needs machinery — the unit submitted to Omega. */ +export interface EquipmentLine { + activity: string; + equipment: string; + qty: number; + unit: string; + fromDate: string; + toDate: string; +} + +/** Weekly equipment/machinery schedule — submitted to Omega (OAEC) via API. */ +@Entity('equipment_schedule') +export class EquipmentScheduleEntity extends BaseAppEntity { + @Index() @Column({ name: 'project_id' }) projectId: string; + @Column({ name: 'week_of', type: 'date' }) weekOf: string; + @Column({ type: 'jsonb' }) lines: EquipmentLine[]; + @Column({ type: 'varchar', default: 'draft' }) status: 'draft' | 'submitted' | 'pushed'; + @Column({ name: 'prepared_by' }) preparedBy: string; + /** OAEC reference returned when pushed to Omega. */ + @Column({ name: 'omega_ref', type: 'varchar', nullable: true }) omegaRef: string | null; + @Column({ name: 'pushed_at', type: 'timestamptz', nullable: true }) pushedAt: Date | null; +} + +class CreateDto { + @IsString() projectId: string; + @IsDateString() weekOf: string; + @IsArray() lines: EquipmentLine[]; +} + +@Injectable() +export class EquipmentService implements OnModuleInit { + private readonly log = new Logger('Equipment'); + + constructor( + @InjectRepository(EquipmentScheduleEntity) private readonly schedules: Repository, + private readonly config: ConfigService, + private readonly registry: ReadModelRegistry, + ) {} + + onModuleInit(): void { + this.registry.register( + { key: 'engineering.equipment-schedule', module: 'engineering', title: 'Weekly equipment schedules', description: 'Machinery/activity schedules, with Omega (OAEC) push status.' }, + () => this.schedules.find({ order: { createdAt: 'DESC' }, take: 100 }), + ); + this.registry.register( + { key: 'omega.equipment-pushes', module: 'operations', title: 'Equipment schedules pushed to Omega', description: 'OAEC integration log — schedules submitted to the Omega fleet system.' }, + () => this.schedules.find({ where: { status: 'pushed' }, order: { pushedAt: 'DESC' }, take: 100 }), + ); + } + + list(): Promise { + return this.schedules.find({ order: { createdAt: 'DESC' }, take: 100 }); + } + + create(user: UserClaims, dto: CreateDto): Promise { + return this.schedules.save( + this.schedules.create({ + projectId: dto.projectId, weekOf: dto.weekOf, lines: dto.lines, + status: 'draft', preparedBy: user.email, entity: user.entity, createdBy: user.email, + }), + ); + } + + async submit(id: string): Promise { + const s = await this.schedules.findOne({ where: { id } }); + if (!s) throw new NotFoundException('no such schedule'); + s.status = 'submitted'; + return this.schedules.save(s); + } + + /** Push to Omega's OAEC system over its API; simulate a ref when not configured. */ + async pushToOmega(id: string): Promise { + const s = await this.schedules.findOne({ where: { id } }); + if (!s) throw new NotFoundException('no such schedule'); + const base = this.config.get('OAEC_BASE_URL'); + let omegaRef: string; + if (base) { + try { + const res = await fetch(`${base.replace(/\/$/, '')}/equipment-schedules`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projectId: s.projectId, weekOf: s.weekOf, lines: s.lines, source: 'UBI' }), + }); + const data = (await res.json()) as { ref?: string }; + omegaRef = data.ref ?? `OAEC-${Date.now()}`; + } catch (e) { + this.log.error(`OAEC push failed: ${String(e)}`); + throw e; + } + } else { + // OAEC not wired yet — record a simulated reference so the flow is visible. + omegaRef = `OAEC-SIM-${(await this.schedules.count({ where: { status: 'pushed' } })) + 1001}`; + this.log.warn(`OAEC_BASE_URL not set — simulated push ${omegaRef}`); + } + s.status = 'pushed'; + s.omegaRef = omegaRef; + s.pushedAt = new Date(); + return this.schedules.save(s); + } + + async seed(user: UserClaims, projectId: string): Promise { + if ((await this.schedules.count()) > 0) return; + const today = new Date(); + const wk = new Date(today); wk.setDate(today.getDate() + (6 - today.getDay())); + const weekOf = wk.toISOString().slice(0, 10); + const d = (off: number) => { const x = new Date(today); x.setDate(today.getDate() + off); return x.toISOString().slice(0, 10); }; + await this.schedules.save( + this.schedules.create({ + projectId, weekOf, status: 'draft', preparedBy: 'seed', createdBy: 'seed', + lines: [ + { activity: 'PCCP pouring STA 4+200–4+600', equipment: 'Transit mixer', qty: 3, unit: 'units', fromDate: d(1), toDate: d(3) }, + { activity: 'Subbase compaction', equipment: 'Vibratory roller', qty: 1, unit: 'unit', fromDate: d(1), toDate: d(5) }, + { activity: 'Hauling aggregates', equipment: 'Dump truck', qty: 4, unit: 'units', fromDate: d(2), toDate: d(6) }, + ], + }), + ); + } +} + +@Controller('equipment') +export class EquipmentController { + constructor(private readonly svc: EquipmentService) {} + + @Get('schedules') list(): Promise { return this.svc.list(); } + @Post('schedules') create(@CurrentUser() u: UserClaims, @Body() dto: CreateDto): Promise { return this.svc.create(u, dto); } + @Post('schedules/:id/submit') submit(@Param('id') id: string): Promise { return this.svc.submit(id); } + @Post('schedules/:id/push') push(@Param('id') id: string): Promise { return this.svc.pushToOmega(id); } +} + +@Module({ + imports: [TypeOrmModule.forFeature([EquipmentScheduleEntity])], + controllers: [EquipmentController], + providers: [EquipmentService], + exports: [EquipmentService], +}) +export class EquipmentModule {} diff --git a/apps/api/src/modules/fleet/fleet.module.ts b/apps/api/src/modules/fleet/fleet.module.ts new file mode 100644 index 0000000..2d9d8cd --- /dev/null +++ b/apps/api/src/modules/fleet/fleet.module.ts @@ -0,0 +1,280 @@ +import { Body, Controller, Get, Injectable, Module, OnModuleInit, Param, Post, Query } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ReadModelRegistry } from '../../pillars/ai-layer/read-model.registry'; + +/** + * Cartrack Fleet API client (HTTP Basic auth, regional base URL). + * Credentials come from env — NEVER committed: + * CARTRACK_REGION (e.g. ph), CARTRACK_USER, CARTRACK_PASS + * Base: https://fleetapi-.cartrack.com/rest + */ +@Injectable() +export class CartrackClient { + private readonly region: string; + private readonly user?: string; + private readonly pass?: string; + private readonly base: string; + + constructor(config: ConfigService) { + this.region = config.get('CARTRACK_REGION', 'ph'); + this.user = config.get('CARTRACK_USER'); + this.pass = config.get('CARTRACK_PASS'); + // KE/SA are on karooooo.com; everyone else cartrack.com. + const host = ['ke', 'sa'].includes(this.region) ? 'karooooo.com' : 'cartrack.com'; + this.base = `https://fleetapi-${this.region}.${host}/rest`; + } + + get configured(): boolean { + return Boolean(this.user && this.pass); + } + + private authHeader(): string { + return 'Basic ' + Buffer.from(`${this.user}:${this.pass}`).toString('base64'); + } + + /** GET a Cartrack path (relative to /rest). Returns parsed JSON or throws. */ + async get(path: string, query?: Record): Promise { + const qs = query + ? '?' + Object.entries(query).filter(([, v]) => v != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join('&') + : ''; + const res = await fetch(`${this.base}${path}${qs}`, { + headers: { Accept: 'application/json', Authorization: this.authHeader() }, + }); + if (!res.ok) throw new Error(`Cartrack ${path} -> ${res.status} ${res.statusText}`); + return (await res.json()) as T; + } + + async post(path: string, body?: unknown): Promise { + const res = await fetch(`${this.base}${path}`, { + method: 'POST', + headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: this.authHeader() }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!res.ok) throw new Error(`Cartrack POST ${path} -> ${res.status} ${res.statusText}`); + return (await res.json()) as T; + } + + /** Try several candidate paths (Cartrack has both /vehicles/status and /vehicle/status/location). */ + async getFirst(paths: string[]): Promise { + let lastErr: unknown; + for (const p of paths) { + try { + return await this.get(p); + } catch (e) { + lastErr = e; + } + } + throw lastErr ?? new Error('no candidate path succeeded'); + } +} + +export interface LiveVehicle { + id: string; + registration: string; + name: string | null; + lat: number | null; + lng: number | null; + speed: number | null; + heading: number | null; + ignition: boolean | null; + odometerKm: number | null; + fuelPct: number | null; + driver: string | null; + address: string | null; + at: string | null; + /** true when this row is demo data because credentials aren't set. */ + demo?: boolean; +} + +const pad = (n: number) => String(n).padStart(2, '0'); +const fmt = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; +const daysAgo = (n: number) => { const d = new Date(); d.setDate(d.getDate() - n); d.setHours(0, 0, 0, 0); return fmt(d); }; +const todayMidnight = () => { const d = new Date(); d.setHours(0, 0, 0, 0); return fmt(d); }; + +@Injectable() +export class FleetService implements OnModuleInit { + constructor( + private readonly cartrack: CartrackClient, + private readonly registry: ReadModelRegistry, + ) {} + + onModuleInit(): void { + this.registry.register( + { key: 'fleet.live', module: 'property', title: 'Fleet — live vehicle positions (Cartrack)', description: 'Real-time GPS position/speed/ignition per vehicle.' }, + () => this.live(), + ); + this.registry.register( + { key: 'fleet.vehicles', module: 'property', title: 'Fleet — vehicle register (Cartrack)', description: 'All tracked vehicles with current status.' }, + () => this.vehicles(), + ); + this.registry.register( + { key: 'fleet.events', module: 'property', title: 'Fleet — driver-behaviour events', description: 'Harsh braking / speeding / harsh acceleration.' }, + () => this.events({}), + ); + this.registry.register( + { key: 'fleet.summary', module: 'property', title: 'Fleet — moving/stopped summary', description: 'Vehicle counts: total / moving / stopped / no-fix.' }, + () => this.summary(), + ); + } + + status(): { configured: boolean; region: string } { + return { configured: this.cartrack.configured, region: process.env.CARTRACK_REGION ?? 'ph' }; + } + + /** Demo fleet (PH coords) shown until CARTRACK_USER/PASS are set. */ + private demoFleet(): LiveVehicle[] { + const now = new Date().toISOString(); + const v = (id: string, registration: string, lat: number, lng: number, speed: number, ignition: boolean, address: string): LiveVehicle => + ({ id, registration, name: registration, lat, lng, speed, heading: 0, ignition, odometerKm: 100000, fuelPct: 60, driver: null, address, at: now, demo: true }); + return [ + v('D1', 'UBI-DT-114', 8.4542, 124.6319, 42, true, 'Cagayan de Oro — Daang Maharlika'), + v('D2', 'UBI-DT-118', 8.228, 124.2452, 0, false, 'Iligan — PKG-02 site yard'), + v('D3', 'UBI-SV-09', 7.1907, 125.4553, 67, true, 'Davao — Mawab-Maco bypass'), + v('D4', 'UBI-BH-21', 8.15, 125.1278, 0, false, 'Bukidnon — PKG-11 yard'), + ]; + } + + private toNum(v: unknown): number | null { + const n = typeof v === 'string' ? parseFloat(v) : (v as number); + return Number.isFinite(n) ? n : null; + } + + /** Normalize Cartrack's /vehicles/status payload. Position is nested under + * `location` (latitude/longitude/position_description/updated). */ + private normalize(rows: Array>): LiveVehicle[] { + return rows.map((r, i) => { + const loc = (r.location ?? {}) as Record; + const fuel = (r.fuel ?? {}) as Record; + const drv = (r.driver ?? {}) as Record; + const driverName = [drv.first_name, drv.last_name].filter(Boolean).join(' ').trim(); + const odoM = this.toNum(r.odometer); + return { + id: String(r.vehicle_id ?? r.id ?? r.terminal_id ?? i), + registration: String(r.registration ?? r.reg ?? r.plate ?? '—'), + name: (r.vehicle_name ?? r.client_vehicle_description ?? null) as string | null, + lat: this.toNum(loc.latitude ?? r.latitude ?? r.lat), + lng: this.toNum(loc.longitude ?? r.longitude ?? r.lng ?? r.lon), + speed: this.toNum(r.speed ?? r.velocity), + heading: this.toNum(r.bearing ?? r.heading ?? r.direction), + ignition: typeof r.ignition === 'boolean' ? r.ignition : r.ignition === 'ON' ? true : r.ignition === 'OFF' ? false : null, + odometerKm: odoM != null ? Math.round(odoM / 1000) : null, + fuelPct: this.toNum(fuel.precentage_left ?? fuel.percentage_left ?? fuel.level), + driver: driverName || null, + address: (loc.position_description ?? r.address ?? null) as string | null, + at: (loc.updated ?? r.event_ts ?? r.gps_time ?? null) as string | null, + }; + }); + } + + private rows(data: unknown): Array> { + if (Array.isArray(data)) return data as Array>; + const d = (data as { data?: unknown })?.data; + return Array.isArray(d) ? (d as Array>) : []; + } + + async live(): Promise { + if (!this.cartrack.configured) return this.demoFleet(); + const data = await this.cartrack.get('/vehicles/status'); + return this.normalize(this.rows(data)); + } + + /** KPI summary for ManCom / Property dashboard. */ + async summary(): Promise<{ configured: boolean; total: number; moving: number; stopped: number; noFix: number }> { + const live = await this.live(); + return { + configured: this.cartrack.configured, + total: live.length, + moving: live.filter((v) => v.ignition === true && (v.speed ?? 0) > 0).length, + stopped: live.filter((v) => !v.ignition || (v.speed ?? 0) === 0).length, + noFix: live.filter((v) => v.lat == null).length, + }; + } + + async locate(id: string): Promise { + if (!this.cartrack.configured) return { demo: true, message: 'set CARTRACK_USER/PASS to request a live fix' }; + return this.cartrack.post(`/vehicle/${id}/locate`); + } + + async vehicles(): Promise>> { + if (!this.cartrack.configured) return this.demoFleet() as unknown as Array>; + return this.rows(await this.cartrack.get('/vehicles')); + } + + async trips(params: { from?: string; to?: string }): Promise>> { + if (!this.cartrack.configured) return []; + const data = await this.cartrack.get('/trips', { + start_timestamp: params.from ?? daysAgo(2), + end_timestamp: params.to ?? fmt(new Date()), + }); + return this.rows(data); + } + + /** Driver-behaviour events. Cartrack requires end_timestamp <= today 00:00:00. */ + async events(params: { from?: string; to?: string }): Promise>> { + if (!this.cartrack.configured) return []; + const data = await this.cartrack.get('/vehicles/events', { + start_timestamp: params.from ?? daysAgo(7), + end_timestamp: params.to ?? todayMidnight(), + }); + return this.rows(data); + } + + /** Alert notifications (geofence/ignition/panic/etc.) — bracket-keyed date filter. */ + async notifications(params: { from?: string; to?: string }): Promise>> { + if (!this.cartrack.configured) return []; + const data = await this.cartrack.get('/alerts/notifications', { + 'filter[date_from]': params.from ?? daysAgo(2), + 'filter[date_to]': params.to ?? fmt(new Date()), + }); + return this.rows(data); + } + + async geofences(): Promise>> { + if (!this.cartrack.configured) return []; + return this.rows(await this.cartrack.get('/geofences')); + } + + async drivers(): Promise>> { + if (!this.cartrack.configured) return []; + return this.rows(await this.cartrack.get('/drivers')); + } + + /** Maintenance/reminders derived from the vehicle register (is_under_maintenance, + * licence expiry) — the dedicated /maintenance path 500s on this account. */ + async maintenance(): Promise>> { + const vs = await this.vehicles(); + return vs + .map((v) => ({ + registration: v.registration, + name: v.vehicle_name ?? v.client_vehicle_description, + under_maintenance: v.is_under_maintenance ?? false, + licence_expiry: v.licence_expiry_date ?? null, + model: [v.manufacturer, v.model].filter(Boolean).join(' '), + })) + .filter((m) => m.under_maintenance || m.licence_expiry); + } +} + +@Controller('fleet') +export class FleetController { + constructor(private readonly svc: FleetService) {} + + @Get('status') status(): { configured: boolean; region: string } { return this.svc.status(); } + @Get('summary') summary(): ReturnType { return this.svc.summary(); } + @Get('live') live(): Promise { return this.svc.live(); } + @Get('vehicles') vehicles(): Promise { return this.svc.vehicles(); } + @Get('geofences') geofences(): Promise { return this.svc.geofences(); } + @Get('drivers') drivers(): Promise { return this.svc.drivers(); } + @Get('maintenance') maintenance(): Promise { return this.svc.maintenance(); } + @Get('events') events(@Query('from') from?: string, @Query('to') to?: string): Promise { return this.svc.events({ from, to }); } + @Get('notifications') notifications(@Query('from') from?: string, @Query('to') to?: string): Promise { return this.svc.notifications({ from, to }); } + @Get('trips') trips(@Query('from') from?: string, @Query('to') to?: string): Promise { return this.svc.trips({ from, to }); } + @Post('vehicles/:id/locate') locate(@Param('id') id: string): Promise { return this.svc.locate(id); } +} + +@Module({ + controllers: [FleetController], + providers: [CartrackClient, FleetService], + exports: [FleetService], +}) +export class FleetModule {} diff --git a/apps/api/src/modules/it/it.module.ts b/apps/api/src/modules/it/it.module.ts new file mode 100644 index 0000000..43d83ba --- /dev/null +++ b/apps/api/src/modules/it/it.module.ts @@ -0,0 +1,267 @@ +import { Body, Controller, Get, Injectable, Module, OnModuleInit, Param, Post, Query } from '@nestjs/common'; +import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import type { UserClaims } from '@ubi/types'; +import { IsArray, IsIn, IsInt, IsOptional, IsString, MinLength } from 'class-validator'; +import { Column, Entity, Index, Repository } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; +import { CurrentUser } from '../../common/rbac'; +import { ReadModelRegistry } from '../../pillars/ai-layer/read-model.registry'; +import { PropertyModule } from '../property/property.module'; +import { PropertyService } from '../property/property.module'; + +/** SNMP-monitored device (UPS, switch, NVR, firewall). */ +@Entity('it_device') +export class DeviceEntity extends BaseAppEntity { + @Column() name: string; + @Column() kind: string; + @Column() ip: string; + @Column({ type: 'varchar', default: 'up' }) status: 'up' | 'warn' | 'down'; + @Column({ name: 'detail', type: 'varchar', nullable: true }) detail: string | null; +} + +export type PmsStatus = 'scheduled' | 'in-progress' | 'done'; + +/** Quarterly preventive-maintenance schedule, auto-built from Property assets. */ +@Entity('it_pms') +export class PmsEntity extends BaseAppEntity { + @Index() @Column({ name: 'asset_qr' }) assetQr: string; + @Column() assetType: string; + @Column({ default: 'General' }) category: string; + @Index() @Column() quarter: string; // e.g. 2026-Q2 + @Column({ name: 'due_on', type: 'date' }) dueOn: string; + @Column({ type: 'varchar', default: 'scheduled' }) status: PmsStatus; + @Column({ type: 'varchar', nullable: true }) tech: string | null; + @Column({ name: 'completed_on', type: 'date', nullable: true }) completedOn: string | null; +} + +/** DeskGuard v2 — self-reporting productivity (client-side OCR/counts posted). */ +@Entity('deskguard_entry') +export class DeskguardEntity extends BaseAppEntity { + @Index() @Column() person: string; + @Column() topApp: string; + @Column({ name: 'kpi_activity' }) kpiActivity: string; + @Column({ name: 'keystrokes', type: 'int', default: 0 }) keystrokes: number; + @Column({ name: 'mouse', type: 'int', default: 0 }) mouse: number; + @Column({ name: 'active_minutes', type: 'int', default: 0 }) activeMinutes: number; + @Column({ type: 'date' }) day: string; +} + +/** Server-room / ambient temperature (DHT22+ESP32 or SNMP/IPMI). */ +@Entity('env_reading') +export class EnvReadingEntity extends BaseAppEntity { + @Column() sensor: string; + @Column({ name: 'temp_c', type: 'real' }) tempC: number; + @Column({ type: 'real', nullable: true }) humidity: number | null; +} + +export const DESKGUARD_CONSENT = + 'Activity capture is for OFFICIAL USE on company-owned assets only. ' + + 'Avoid personal activity on company devices. Captured data is role-scoped.'; + +class DeviceDto { + @IsString() @MinLength(2) name: string; + @IsString() @MinLength(2) kind: string; + @IsString() @MinLength(2) ip: string; +} +class DeskguardDto { + @IsString() person: string; + @IsString() topApp: string; + @IsString() kpiActivity: string; + @IsOptional() @IsInt() keystrokes?: number; + @IsOptional() @IsInt() mouse?: number; + @IsOptional() @IsInt() activeMinutes?: number; +} + +class GeneratePmsDto { + /** e.g. 2026-Q3. Defaults to current quarter. */ + @IsOptional() @IsString() quarter?: string; + /** Asset categories to include; empty = all PMS-eligible. */ + @IsOptional() @IsArray() @IsString({ each: true }) categories?: string[]; +} + +class PmsStatusDto { + @IsIn(['scheduled', 'in-progress', 'done']) status: PmsStatus; + @IsOptional() @IsString() tech?: string; +} + +@Injectable() +export class ItService implements OnModuleInit { + constructor( + @InjectRepository(DeviceEntity) private readonly devices: Repository, + @InjectRepository(PmsEntity) private readonly pms: Repository, + @InjectRepository(DeskguardEntity) private readonly desk: Repository, + @InjectRepository(EnvReadingEntity) private readonly env: Repository, + private readonly property: PropertyService, + private readonly registry: ReadModelRegistry, + ) {} + + onModuleInit(): void { + this.registry.register( + { key: 'it.capacity', module: 'it', title: 'Capacity planning & controls', description: 'Resource headroom + projected breach dates.' }, + () => this.capacity(), + ); + this.registry.register( + { key: 'it.devices', module: 'it', title: 'Monitored devices (SNMP)', description: 'UPS/switch/NVR/firewall status.' }, + () => this.devices.find({ order: { kind: 'ASC' } }), + ); + this.registry.register( + { key: 'it.pms', module: 'it', title: 'Preventive maintenance schedule', description: 'Quarterly PMS auto-built from active Property assets.' }, + () => this.pms.find({ order: { dueOn: 'ASC' }, take: 200 }), + ); + this.registry.register( + { key: 'it.deskguard', module: 'it', title: 'DeskGuard activity (self-report)', description: 'Top apps / KPI hours; official-use-only, role-scoped.' }, + () => this.desk.find({ order: { createdAt: 'DESC' }, take: 100 }), + ); + this.registry.register( + { key: 'it.env', module: 'it', title: 'Environmental monitoring', description: 'Server-room + ambient temperature/humidity.' }, + () => this.env.find({ order: { createdAt: 'DESC' }, take: 50 }), + ); + } + + /** Stub capacity figures (the box) — real SNMP/df feed lands later. */ + capacity(): Array> { + return [ + { resource: 'Storage (320 GB SSD)', usedPct: 41, trend: 'rising', projectedBreach: '2027-02', status: 'green' }, + { resource: 'Data transfer (6 TB/mo)', usedPct: 22, trend: 'stable', projectedBreach: null, status: 'green' }, + { resource: 'Postgres size', usedPct: 18, trend: 'rising', projectedBreach: null, status: 'green' }, + { resource: 'UPS load vs rated', usedPct: 63, trend: 'stable', projectedBreach: null, status: 'amber' }, + ]; + } + + listDevices(): Promise { return this.devices.find({ order: { kind: 'ASC' } }); } + listEnv(): Promise { return this.env.find({ order: { createdAt: 'DESC' }, take: 50 }); } + listDeskguard(): Promise { return this.desk.find({ order: { createdAt: 'DESC' }, take: 100 }); } + + listPms(quarter?: string): Promise { + return this.pms.find({ where: quarter ? { quarter } : {}, order: { dueOn: 'ASC' }, take: 500 }); + } + + pmsCategories(): Promise> { + return this.property.categories(); + } + + async setPmsStatus(id: string, dto: { status: PmsStatus; tech?: string }): Promise { + const patch: Partial = { status: dto.status }; + if (dto.tech !== undefined) patch.tech = dto.tech; + patch.completedOn = dto.status === 'done' ? new Date().toISOString().slice(0, 10) : null; + await this.pms.update(id, patch); + return this.pms.findOne({ where: { id } }); + } + + private currentQuarter(): string { + const n = new Date(); + return `${n.getFullYear()}-Q${Math.floor(n.getMonth() / 3) + 1}`; + } + + /** Due date = 28th of the last month of the quarter. */ + private quarterDue(quarter: string): string { + const [yStr, qStr] = quarter.split('-Q'); + const year = Number(yStr); + const q = Number(qStr); + return new Date(year, q * 3 - 1, 28).toISOString().slice(0, 10); + } + + addDevice(user: UserClaims, dto: DeviceDto): Promise { + return this.devices.save(this.devices.create({ ...dto, status: 'up', createdBy: user.email })); + } + + /** Generate quarterly PMS for selected asset categories (idempotent/quarter). */ + async generatePms( + user: UserClaims, + opts: { quarter?: string; categories?: string[] } = {}, + ): Promise<{ created: number; quarter: string }> { + const q = opts.quarter ?? this.currentQuarter(); + const dueOn = this.quarterDue(q); + const assets = await this.property.pmsEligibleByCategories(opts.categories ?? []); + let created = 0; + for (const a of assets) { + const exists = await this.pms.findOne({ where: { assetQr: a.qr, quarter: q } }); + if (exists) continue; + await this.pms.save( + this.pms.create({ + assetQr: a.qr, assetType: a.type, category: a.category, quarter: q, + dueOn, status: 'scheduled', tech: null, completedOn: null, createdBy: user?.email ?? 'system', + }), + ); + created += 1; + } + return { created, quarter: q }; + } + + postDeskguard(user: UserClaims, dto: DeskguardDto): Promise { + return this.desk.save( + this.desk.create({ + person: dto.person, topApp: dto.topApp, kpiActivity: dto.kpiActivity, + keystrokes: dto.keystrokes ?? 0, mouse: dto.mouse ?? 0, activeMinutes: dto.activeMinutes ?? 0, + day: new Date().toISOString().slice(0, 10), createdBy: user.email, + }), + ); + } + + async seed(user: UserClaims): Promise { + if ((await this.devices.count()) === 0) { + await this.devices.save([ + this.devices.create({ name: 'APC UPS — server room', kind: 'UPS', ip: '10.0.0.11', status: 'up', detail: 'load 63%, runtime 22m', createdBy: 'seed' }), + this.devices.create({ name: 'H3C core switch', kind: 'Switch', ip: '10.0.0.1', status: 'up', detail: '48 ports, 31 active', createdBy: 'seed' }), + this.devices.create({ name: 'Hikvision NVR', kind: 'NVR', ip: '10.0.0.30', status: 'warn', detail: '1 camera offline (CAM-IT-01)', createdBy: 'seed' }), + this.devices.create({ name: 'Sangfor NGAF', kind: 'Firewall', ip: '10.0.0.2', status: 'up', detail: 'threats blocked 24h: 142', createdBy: 'seed' }), + ]); + } + if ((await this.env.count()) === 0) { + await this.env.save([ + this.env.create({ sensor: 'Server room (DHT22-1)', tempC: 23.4, humidity: 51, createdBy: 'seed' }), + this.env.create({ sensor: 'Server room (IPMI)', tempC: 24.1, humidity: null, createdBy: 'seed' }), + ]); + } + if ((await this.desk.count()) === 0) { + await this.desk.save([ + this.desk.create({ person: 'Encoder — Cruz', topApp: 'Acumatica', kpiActivity: 'DR encoding', keystrokes: 14200, mouse: 3100, activeMinutes: 372, day: new Date().toISOString().slice(0, 10), createdBy: 'seed' }), + ]); + } + // Seed current + previous quarter so all PMS views have content. + const now = new Date(); + const y = now.getFullYear(); + const curQ = Math.floor(now.getMonth() / 3) + 1; + await this.generatePms(user, { quarter: `${y}-Q${curQ}` }); + if (curQ > 1) await this.generatePms(user, { quarter: `${y}-Q${curQ - 1}` }); + + // Give a few items tech + status so kanban/calendar/list aren't all "scheduled". + const sample = await this.pms.find({ order: { dueOn: 'ASC' }, take: 4 }); + const techs = ['Tech — Reyes', 'Tech — Santos']; + for (let i = 0; i < sample.length; i++) { + const s = i === 0 ? 'done' : i === 1 ? 'in-progress' : 'scheduled'; + await this.setPmsStatus(sample[i].id, { status: s as PmsStatus, tech: techs[i % 2] }); + } + } +} + +@Controller('it') +export class ItController { + constructor(private readonly svc: ItService) {} + + @Get('capacity') capacity(): unknown { return this.svc.capacity(); } + @Get('devices') devices(): Promise { return this.svc.listDevices(); } + @Get('env') env(): Promise { return this.svc.listEnv(); } + @Get('deskguard/policy') policy(): { consent: string } { return { consent: DESKGUARD_CONSENT }; } + @Get('deskguard') deskguard(): Promise { return this.svc.listDeskguard(); } + + @Get('pms') pmsList(@Query('quarter') quarter?: string): Promise { return this.svc.listPms(quarter); } + @Get('pms/categories') pmsCats(): Promise> { return this.svc.pmsCategories(); } + @Post('pms/generate') gen(@CurrentUser() u: UserClaims, @Body() dto: GeneratePmsDto): Promise<{ created: number; quarter: string }> { + return this.svc.generatePms(u, { quarter: dto.quarter, categories: dto.categories }); + } + @Post('pms/:id/status') setStatus(@Param('id') id: string, @Body() dto: PmsStatusDto): Promise { + return this.svc.setPmsStatus(id, dto); + } + + @Post('devices') addDevice(@CurrentUser() u: UserClaims, @Body() dto: DeviceDto): Promise { return this.svc.addDevice(u, dto); } + @Post('deskguard') post(@CurrentUser() u: UserClaims, @Body() dto: DeskguardDto): Promise { return this.svc.postDeskguard(u, dto); } +} + +@Module({ + imports: [TypeOrmModule.forFeature([DeviceEntity, PmsEntity, DeskguardEntity, EnvReadingEntity]), PropertyModule], + controllers: [ItController], + providers: [ItService], + exports: [ItService], +}) +export class ItModule {} diff --git a/apps/api/src/modules/mqc/mqc.module.ts b/apps/api/src/modules/mqc/mqc.module.ts new file mode 100644 index 0000000..e002da3 --- /dev/null +++ b/apps/api/src/modules/mqc/mqc.module.ts @@ -0,0 +1,305 @@ +import { + Body, + Controller, + Get, + Injectable, + Module, + OnModuleInit, + Param, + Post, + Query, +} from '@nestjs/common'; +import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import { + Column, + Entity, + Repository, +} from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; +import { Claims } from '../../common/rbac'; +import { ReadModelRegistry } from '../../pillars/ai-layer/read-model.registry'; +import { AiLayerModule } from '../../pillars/ai-layer/ai-layer.module'; + +// ──────────────────────── DPWH minimum testing rules ─────────────────────── + +/** Minimum DPWH testing requirements per pay item (from Blue Book). */ +export const DPWH_TEST_RULES: Record = { + '311(1)a': [ + { test: 'Slump test', per: 'truck', qty: 1 }, + { test: 'Air content', per: 'truck', qty: 1 }, + { test: 'Concrete cylinder (7-day)', per: '50 m³', qty: 2 }, + { test: 'Concrete cylinder (28-day)', per: '50 m³', qty: 2 }, + ], + '311(1)b': [ + { test: 'Slump test', per: 'truck', qty: 1 }, + { test: 'Concrete beam (7-day)', per: '150 m³', qty: 2 }, + { test: 'Concrete beam (28-day)', per: '150 m³', qty: 2 }, + ], + '311(1)c': [ + { test: 'Slump test', per: 'truck', qty: 1 }, + { test: 'Air content', per: 'truck', qty: 1 }, + { test: 'Concrete beam (7-day)', per: '150 m³', qty: 2 }, + { test: 'Concrete beam (14-day)', per: '150 m³', qty: 2 }, + { test: 'Concrete beam (28-day)', per: '150 m³', qty: 2 }, + ], + '200': [ + { test: 'CBR laboratory', per: 'source change', qty: 1 }, + { test: 'Field density (nuclear)', per: '500 m²', qty: 1 }, + ], + '201': [ + { test: 'Field density', per: '500 m²', qty: 1 }, + { test: 'Atterberg limits', per: 'source change', qty: 1 }, + ], + '301': [ + { test: 'Gradation', per: 'lot', qty: 1 }, + { test: 'CBR', per: 'source change', qty: 1 }, + { test: 'Field density', per: '500 m²', qty: 1 }, + ], +}; + +// ──────────────────────────── Entities ──────────────────────────── + +/** A single QC test result. */ +@Entity('mqc_test') +export class QcTest extends BaseAppEntity { + @Column({ name: 'project_id', type: 'varchar' }) + projectId: string; + + /** DPWH pay-item number the test covers, e.g. "311(1)c". */ + @Column({ name: 'pay_item_no', type: 'varchar', length: 32 }) + payItemNo: string; + + @Column({ name: 'test_type', type: 'varchar', length: 64 }) + testType: string; + + /** Lab-assigned reference number. */ + @Column({ name: 'lab_ref', type: 'varchar', nullable: true }) + labRef: string | null; + + @Column({ name: 'cert_id', type: 'varchar', nullable: true }) + certId: string | null; + + /** passed | failed | pending */ + @Column({ type: 'varchar', length: 16, default: 'pending' }) + result: string; + + @Column({ name: 'test_date', type: 'date', nullable: true }) + testDate: Date | null; + + /** Which billing batch this cert will attach to. */ + @Column({ name: 'billing_ref', type: 'varchar', nullable: true }) + billingRef: string | null; + + @Column({ name: 'notes', type: 'text', nullable: true }) + notes: string | null; +} + +/** Concrete pour log — standardised DPWH form. */ +@Entity('mqc_pour_log') +export class PourLog extends BaseAppEntity { + @Column({ name: 'project_id', type: 'varchar' }) + projectId: string; + + @Column({ name: 'pay_item_no', type: 'varchar', length: 32 }) + payItemNo: string; + + /** STA 0+000 format — location of the pour. */ + @Column({ name: 'station', type: 'varchar', length: 16, nullable: true }) + station: string | null; + + @Column({ name: 'pour_date', type: 'date' }) + pourDate: Date; + + /** Mix design code / specification. */ + @Column({ name: 'mix_design', type: 'varchar', length: 64, nullable: true }) + mixDesign: string | null; + + /** Volume poured (m³). */ + @Column({ name: 'volume_m3', type: 'numeric', precision: 10, scale: 3 }) + volumeM3: number; + + /** Slump mm at point of placement. */ + @Column({ name: 'slump_mm', type: 'numeric', precision: 6, scale: 1, nullable: true }) + slumpMm: number | null; + + @Column({ name: 'air_content_pct', type: 'numeric', precision: 4, scale: 2, nullable: true }) + airContentPct: number | null; + + @Column({ name: 'inspector_id', type: 'varchar', nullable: true }) + inspectorId: string | null; + + @Column({ name: 'notes', type: 'text', nullable: true }) + notes: string | null; +} + +/** Issued material certificate — attaches to a billing record via doc tracking. */ +@Entity('mqc_material_cert') +export class MaterialCert extends BaseAppEntity { + @Column({ name: 'cert_no', type: 'varchar', length: 32, unique: true }) + certNo: string; + + @Column({ name: 'project_id', type: 'varchar' }) + projectId: string; + + @Column({ name: 'pay_item_no', type: 'varchar', length: 32 }) + payItemNo: string; + + /** Billing reference the cert is attached to. */ + @Column({ name: 'billing_ref', type: 'varchar', nullable: true }) + billingRef: string | null; + + @Column({ name: 'issued_date', type: 'date' }) + issuedDate: Date; + + /** pending | issued | rejected */ + @Column({ type: 'varchar', length: 16, default: 'issued' }) + status: string; + + /** Path to the scanned cert file (via StorageProvider). */ + @Column({ name: 'file_path', type: 'varchar', nullable: true }) + filePath: string | null; +} + +// ──────────────────────────── Service ──────────────────────────── + +@Injectable() +export class MqcService implements OnModuleInit { + constructor( + @InjectRepository(QcTest) private readonly tests: Repository, + @InjectRepository(PourLog) private readonly pours: Repository, + @InjectRepository(MaterialCert) private readonly certs: Repository, + private readonly registry: ReadModelRegistry, + ) {} + + onModuleInit(): void { + this.registry.register( + { + key: 'mqc.certs', + module: 'mqc', + title: 'Material certificates queue', + description: 'DPWH minimum testing per pay item; pending certs attach to billings.', + }, + async () => { + const pending = await this.tests.find({ + where: { result: 'pending' }, + order: { createdAt: 'ASC' }, + }); + return pending.map((t) => ({ + certId: t.certId ?? t.id.slice(0, 12).toUpperCase(), + payItemNo: t.payItemNo, + test: t.testType, + status: t.result, + daysPending: Math.floor((Date.now() - t.createdAt.getTime()) / 86_400_000), + billing: t.billingRef ?? '—', + })); + }, + ); + } + + testRules(payItemNo: string) { + return DPWH_TEST_RULES[payItemNo] ?? []; + } + + async listTests(projectId: string, payItemNo?: string): Promise { + const where: Record = { projectId }; + if (payItemNo) where.payItemNo = payItemNo; + return this.tests.find({ where, order: { testDate: 'DESC' } }); + } + + async submitTest(data: Partial, actor: string): Promise { + const t = this.tests.create({ ...data, createdBy: actor }); + return this.tests.save(t); + } + + async listPours(projectId: string): Promise { + return this.pours.find({ where: { projectId }, order: { pourDate: 'DESC' } }); + } + + async submitPour(data: Partial, actor: string): Promise { + const p = this.pours.create({ ...data, createdBy: actor }); + return this.pours.save(p); + } + + async listCerts(projectId: string): Promise { + return this.certs.find({ where: { projectId }, order: { issuedDate: 'DESC' } }); + } + + async issueCert(data: Partial, actor: string): Promise { + const c = this.certs.create({ ...data, createdBy: actor }); + return this.certs.save(c); + } +} + +// ──────────────────────────── Controller ──────────────────────────── + +@Controller('mqc') +export class MqcController { + constructor(private readonly mqc: MqcService) {} + + /** DPWH minimum testing requirements for a pay item. */ + @Get('rules/:payItemNo') + rules(@Param('payItemNo') payItemNo: string) { + return { payItemNo, requirements: this.mqc.testRules(payItemNo) }; + } + + @Get('projects/:projectId/tests') + @Claims('dept:mqc', 'dept:engineering', 'role:manager', 'role:ceo', 'role:vpo') + listTests( + @Param('projectId') projectId: string, + @Query('payItemNo') payItemNo?: string, + ): Promise { + return this.mqc.listTests(projectId, payItemNo); + } + + @Post('projects/:projectId/tests') + @Claims('dept:mqc', 'role:manager') + submitTest( + @Param('projectId') projectId: string, + @Body() body: Partial, + ): Promise { + return this.mqc.submitTest({ ...body, projectId }, 'system'); + } + + @Get('projects/:projectId/pours') + @Claims('dept:mqc', 'dept:engineering', 'role:manager', 'role:ceo', 'role:vpo') + listPours(@Param('projectId') projectId: string): Promise { + return this.mqc.listPours(projectId); + } + + @Post('projects/:projectId/pours') + @Claims('dept:mqc', 'role:manager') + submitPour( + @Param('projectId') projectId: string, + @Body() body: Partial, + ): Promise { + return this.mqc.submitPour({ ...body, projectId }, 'system'); + } + + @Get('projects/:projectId/certs') + @Claims('dept:mqc', 'dept:engineering', 'dept:billing', 'role:manager', 'role:ceo', 'role:vpo') + listCerts(@Param('projectId') projectId: string): Promise { + return this.mqc.listCerts(projectId); + } + + @Post('projects/:projectId/certs') + @Claims('dept:mqc', 'role:manager') + issueCert( + @Param('projectId') projectId: string, + @Body() body: Partial, + ): Promise { + return this.mqc.issueCert({ ...body, projectId }, 'system'); + } +} + +// ──────────────────────────── Module ──────────────────────────── + +@Module({ + imports: [ + TypeOrmModule.forFeature([QcTest, PourLog, MaterialCert]), + AiLayerModule, + ], + controllers: [MqcController], + providers: [MqcService], + exports: [MqcService], +}) +export class MqcModule {} diff --git a/apps/api/src/modules/omega/omega.module.ts b/apps/api/src/modules/omega/omega.module.ts new file mode 100644 index 0000000..ef8a627 --- /dev/null +++ b/apps/api/src/modules/omega/omega.module.ts @@ -0,0 +1,651 @@ +import { + Body, + Controller, + Get, + Injectable, + Logger, + Module, + OnModuleDestroy, + OnModuleInit, + Param, + Post, + Query, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import { Column, Entity, Index, LessThan, Repository } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; +import { Claims } from '../../common/rbac'; +import { ReadModelRegistry } from '../../pillars/ai-layer/read-model.registry'; +import { AiLayerModule } from '../../pillars/ai-layer/ai-layer.module'; + +// ──────────────────────────── Cartrack HTTP client ───────────────────────── + +const pad = (n: number) => String(n).padStart(2, '0'); +const fmt = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; +const daysAgo = (n: number) => { const d = new Date(); d.setDate(d.getDate() - n); d.setHours(0, 0, 0, 0); return fmt(d); }; +const todayMidnight = () => { const d = new Date(); d.setHours(0, 0, 0, 0); return fmt(d); }; + +@Injectable() +export class CartrackClient { + private readonly user?: string; + private readonly pass?: string; + readonly region: string; + readonly base: string; + + constructor(config: ConfigService) { + this.region = config.get('CARTRACK_REGION', 'ph'); + this.user = config.get('CARTRACK_USER'); + this.pass = config.get('CARTRACK_PASS'); + const host = ['ke', 'sa'].includes(this.region) ? 'karooooo.com' : 'cartrack.com'; + this.base = `https://fleetapi-${this.region}.${host}/rest`; + } + + get configured(): boolean { return Boolean(this.user && this.pass); } + + private auth(): string { + return 'Basic ' + Buffer.from(`${this.user}:${this.pass}`).toString('base64'); + } + + async get(path: string, query?: Record): Promise { + const qs = query + ? '?' + Object.entries(query).filter(([, v]) => v != null).map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join('&') + : ''; + const res = await fetch(`${this.base}${path}${qs}`, { + headers: { Accept: 'application/json', Authorization: this.auth() }, + }); + if (!res.ok) throw new Error(`Cartrack ${path} → ${res.status}`); + return (await res.json()) as T; + } + + async post(path: string, body?: unknown): Promise { + const res = await fetch(`${this.base}${path}`, { + method: 'POST', + headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: this.auth() }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!res.ok) throw new Error(`Cartrack POST ${path} → ${res.status}`); + return (await res.json()) as T; + } +} + +// ──────────────────────────── Entities ───────────────────────────────────── + +/** Periodic position snapshot for analytics / history replay. */ +@Entity('fleet_position_log') +export class FleetPositionLog extends BaseAppEntity { + @Index() @Column({ name: 'vehicle_id', type: 'varchar' }) vehicleId: string; + @Column({ name: 'registration', type: 'varchar' }) registration: string; + @Column({ name: 'lat', type: 'numeric', precision: 10, scale: 6, nullable: true }) lat: number | null; + @Column({ name: 'lng', type: 'numeric', precision: 10, scale: 6, nullable: true }) lng: number | null; + @Column({ name: 'speed_kmh', type: 'numeric', precision: 6, scale: 1, nullable: true }) speedKmh: number | null; + @Column({ name: 'heading', type: 'smallint', nullable: true }) heading: number | null; + @Column({ name: 'ignition', type: 'boolean', nullable: true }) ignition: boolean | null; + @Column({ name: 'odometer_km', type: 'int', nullable: true }) odometerKm: number | null; + @Column({ name: 'fuel_pct', type: 'numeric', precision: 5, scale: 2, nullable: true }) fuelPct: number | null; + @Column({ name: 'driver_name', type: 'varchar', nullable: true }) driverName: string | null; + @Column({ name: 'address', type: 'varchar', nullable: true }) address: string | null; + @Index() @Column({ name: 'polled_at', type: 'timestamptz' }) polledAt: Date; +} + +/** Driver-behaviour events (harsh braking, speeding, harsh acceleration). */ +@Entity('fleet_driver_event') +export class FleetDriverEvent extends BaseAppEntity { + @Index() @Column({ name: 'vehicle_id', type: 'varchar' }) vehicleId: string; + @Column({ name: 'registration', type: 'varchar' }) registration: string; + @Column({ name: 'event_type', type: 'varchar', length: 64 }) eventType: string; + @Column({ name: 'event_description', type: 'varchar', nullable: true }) eventDescription: string | null; + @Column({ name: 'severity', type: 'varchar', length: 16, default: 'warning' }) severity: string; + @Column({ name: 'speed_kmh', type: 'numeric', precision: 6, scale: 1, nullable: true }) speedKmh: number | null; + @Column({ name: 'lat', type: 'numeric', precision: 10, scale: 6, nullable: true }) lat: number | null; + @Column({ name: 'lng', type: 'numeric', precision: 10, scale: 6, nullable: true }) lng: number | null; + @Column({ name: 'address', type: 'varchar', nullable: true }) address: string | null; + @Index() @Column({ name: 'occurred_at', type: 'timestamptz' }) occurredAt: Date; + @Column({ name: 'raw', type: 'jsonb', nullable: true }) raw: Record | null; +} + +/** Alerts / notifications (geofence, ignition, panic, speeding threshold, etc.). */ +@Entity('fleet_alert') +export class FleetAlert extends BaseAppEntity { + @Index() @Column({ name: 'vehicle_id', type: 'varchar', nullable: true }) vehicleId: string | null; + @Column({ name: 'registration', type: 'varchar', nullable: true }) registration: string | null; + @Column({ name: 'alert_type', type: 'varchar', length: 64, nullable: true }) alertType: string | null; + @Column({ name: 'message', type: 'text', nullable: true }) message: string | null; + @Column({ name: 'lat', type: 'numeric', precision: 10, scale: 6, nullable: true }) lat: number | null; + @Column({ name: 'lng', type: 'numeric', precision: 10, scale: 6, nullable: true }) lng: number | null; + @Index() @Column({ name: 'occurred_at', type: 'timestamptz' }) occurredAt: Date; + @Column({ name: 'acknowledged', type: 'boolean', default: false }) acknowledged: boolean; + @Column({ name: 'raw', type: 'jsonb', nullable: true }) raw: Record | null; +} + +/** Omega Asia equipment register — master list of OAEC-owned units. */ +@Entity('omega_equipment') +export class OmegaEquipment extends BaseAppEntity { + @Column({ name: 'unit_no', type: 'varchar', unique: true }) unitNo: string; + @Column({ name: 'plate', type: 'varchar', nullable: true }) plate: string | null; + @Column({ name: 'equipment_type', type: 'varchar', length: 64 }) equipmentType: string; + @Column({ name: 'make', type: 'varchar', nullable: true }) make: string | null; + @Column({ name: 'model', type: 'varchar', nullable: true }) model: string | null; + @Column({ name: 'year', type: 'smallint', nullable: true }) year: number | null; + /** Cartrack vehicle_id for linking live telemetry. */ + @Column({ name: 'cartrack_id', type: 'varchar', nullable: true }) cartrackId: string | null; + /** active | maintenance | retired | standby */ + @Column({ type: 'varchar', length: 16, default: 'active' }) status: string; + /** UBI project currently using this unit. */ + @Column({ name: 'assigned_project', type: 'varchar', nullable: true }) assignedProject: string | null; + @Column({ name: 'notes', type: 'text', nullable: true }) notes: string | null; +} + +// ──────────────────────────── Live vehicle shape ──────────────────────────── + +export interface LiveVehicle { + id: string; + registration: string; + name: string | null; + lat: number | null; + lng: number | null; + speed: number | null; + heading: number | null; + ignition: boolean | null; + odometerKm: number | null; + fuelPct: number | null; + driver: string | null; + address: string | null; + at: string | null; + demo?: boolean; +} + +// ──────────────────────────── In-memory live cache ───────────────────────── + +@Injectable() +export class FleetCache { + private _positions: LiveVehicle[] = []; + private _at: number = 0; + + set(positions: LiveVehicle[]): void { this._positions = positions; this._at = Date.now(); } + get(): LiveVehicle[] { return this._positions; } + ageMs(): number { return Date.now() - this._at; } + fresh(): boolean { return this._at > 0 && this.ageMs() < 60_000; } +} + +// ──────────────────────────── Normaliser ─────────────────────────────────── + +@Injectable() +export class FleetNormaliser { + toNum(v: unknown): number | null { + const n = typeof v === 'string' ? parseFloat(v) : (v as number); + return Number.isFinite(n) ? n : null; + } + + rows(data: unknown): Array> { + if (Array.isArray(data)) return data as Array>; + const d = (data as { data?: unknown })?.data; + return Array.isArray(d) ? (d as Array>) : []; + } + + normalize(rows: Array>): LiveVehicle[] { + return rows.map((r, i) => { + const loc = (r.location ?? {}) as Record; + const fuel = (r.fuel ?? {}) as Record; + const drv = (r.driver ?? {}) as Record; + const driverName = [drv.first_name, drv.last_name].filter(Boolean).join(' ').trim(); + const odoM = this.toNum(r.odometer); + return { + id: String(r.vehicle_id ?? r.id ?? r.terminal_id ?? i), + registration: String(r.registration ?? r.reg ?? r.plate ?? '—'), + name: (r.vehicle_name ?? r.client_vehicle_description ?? null) as string | null, + lat: this.toNum(loc.latitude ?? r.latitude ?? r.lat), + lng: this.toNum(loc.longitude ?? r.longitude ?? r.lng ?? r.lon), + speed: this.toNum(r.speed ?? r.velocity), + heading: this.toNum(r.bearing ?? r.heading ?? r.direction), + ignition: r.ignition === true || r.ignition === 'ON' ? true : r.ignition === false || r.ignition === 'OFF' ? false : null, + odometerKm: odoM != null ? Math.round(odoM / 1000) : null, + fuelPct: this.toNum(fuel.precentage_left ?? fuel.percentage_left ?? fuel.level), + driver: driverName || null, + address: (loc.position_description ?? r.address ?? null) as string | null, + at: (loc.updated ?? r.event_ts ?? r.gps_time ?? null) as string | null, + }; + }); + } +} + +// ──────────────────────────── Background poller ──────────────────────────── + +/** Pulls live positions every 10 s (cache) + stores a DB snapshot every 60 s. + * Pulls events + alerts from Cartrack every 10 min and stores to DB. + * All intervals cleared on shutdown so tests/restarts don't leak. */ +@Injectable() +export class FleetPoller implements OnModuleInit, OnModuleDestroy { + private readonly log = new Logger('FleetPoller'); + private liveTimer?: NodeJS.Timeout; + private snapshotTimer?: NodeJS.Timeout; + private analyticsTimer?: NodeJS.Timeout; + private lastAnalyticsPull = 0; + + constructor( + private readonly cartrack: CartrackClient, + private readonly cache: FleetCache, + private readonly norm: FleetNormaliser, + @InjectRepository(FleetPositionLog) private readonly posLog: Repository, + @InjectRepository(FleetDriverEvent) private readonly driverEvents: Repository, + @InjectRepository(FleetAlert) private readonly alerts: Repository, + ) {} + + onModuleInit(): void { + if (!this.cartrack.configured) { + this.log.warn('CARTRACK_USER/PASS not set — poller inactive, demo data served'); + return; + } + // Kick off immediately, then on interval. + void this.pollLive(); + this.liveTimer = setInterval(() => void this.pollLive(), 10_000); + + // DB snapshot every 60 s. + this.snapshotTimer = setInterval(() => void this.saveSnapshot(), 60_000); + + // Analytics (events + alerts) every 10 min. + void this.pollAnalytics(); + this.analyticsTimer = setInterval(() => void this.pollAnalytics(), 10 * 60_000); + + this.log.log('Fleet poller started (live 10s · snapshot 60s · analytics 10min)'); + } + + onModuleDestroy(): void { + clearInterval(this.liveTimer); + clearInterval(this.snapshotTimer); + clearInterval(this.analyticsTimer); + } + + async pollLive(): Promise { + try { + const data = await this.cartrack.get('/vehicles/status'); + this.cache.set(this.norm.normalize(this.norm.rows(data))); + } catch (e) { + this.log.warn(`live poll failed: ${(e as Error).message}`); + } + } + + async saveSnapshot(): Promise { + const positions = this.cache.get(); + if (!positions.length) return; + const now = new Date(); + await this.posLog.save( + positions + .filter((v) => v.lat != null && v.lng != null) + .map((v) => + this.posLog.create({ + vehicleId: v.id, + registration: v.registration, + lat: v.lat, + lng: v.lng, + speedKmh: v.speed, + heading: v.heading, + ignition: v.ignition, + odometerKm: v.odometerKm, + fuelPct: v.fuelPct, + driverName: v.driver, + address: v.address, + polledAt: now, + createdBy: 'poller', + }), + ), + ); + // Purge position log older than 30 days. + const cutoff = new Date(Date.now() - 30 * 86_400_000); + await this.posLog.delete({ polledAt: LessThan(cutoff) }); + } + + async pollAnalytics(): Promise { + const from = this.lastAnalyticsPull + ? fmt(new Date(this.lastAnalyticsPull)) + : daysAgo(1); + this.lastAnalyticsPull = Date.now(); + + // Driver events. + try { + const raw = await this.cartrack.get('/vehicles/events', { + start_timestamp: from, + end_timestamp: todayMidnight(), + }); + const rows = this.norm.rows(raw); + if (rows.length) { + await this.driverEvents.save( + rows.map((r) => + this.driverEvents.create({ + vehicleId: String(r.vehicle_id ?? r.id ?? ''), + registration: String(r.registration ?? '—'), + eventType: String(r.event_type ?? r.type ?? 'unknown'), + eventDescription: (r.event_description ?? r.description ?? null) as string | null, + severity: String(r.severity ?? 'warning'), + speedKmh: this.norm.toNum(r.speed), + lat: this.norm.toNum(r.latitude ?? r.lat), + lng: this.norm.toNum(r.longitude ?? r.lng), + address: (r.position_description ?? r.address ?? null) as string | null, + occurredAt: new Date((r.event_ts ?? r.timestamp ?? Date.now()) as string | number), + raw: r, + createdBy: 'poller', + }), + ), + ); + this.log.log(`analytics: stored ${rows.length} driver events`); + } + } catch (e) { + this.log.warn(`events poll failed: ${(e as Error).message}`); + } + + // Alerts / notifications. + try { + const raw = await this.cartrack.get('/alerts/notifications', { + 'filter[date_from]': from, + 'filter[date_to]': fmt(new Date()), + }); + const rows = this.norm.rows(raw); + if (rows.length) { + await this.alerts.save( + rows.map((r) => + this.alerts.create({ + vehicleId: (r.vehicle_id ?? r.id ?? null) as string | null, + registration: (r.registration ?? null) as string | null, + alertType: (r.trigger_description ?? r.alert_type ?? null) as string | null, + message: (r.notification_msg ?? r.message ?? null) as string | null, + lat: this.norm.toNum(r.latitude ?? r.lat), + lng: this.norm.toNum(r.longitude ?? r.lng), + occurredAt: new Date((r.event_ts ?? r.timestamp ?? Date.now()) as string | number), + raw: r, + createdBy: 'poller', + }), + ), + ); + this.log.log(`analytics: stored ${rows.length} alerts`); + } + } catch (e) { + this.log.warn(`alerts poll failed: ${(e as Error).message}`); + } + } +} + +// ──────────────────────────── Fleet service ──────────────────────────────── + +@Injectable() +export class FleetService implements OnModuleInit { + constructor( + private readonly cartrack: CartrackClient, + private readonly cache: FleetCache, + private readonly norm: FleetNormaliser, + private readonly registry: ReadModelRegistry, + @InjectRepository(FleetPositionLog) private readonly posLog: Repository, + @InjectRepository(FleetDriverEvent) private readonly driverEvents: Repository, + @InjectRepository(FleetAlert) private readonly alerts: Repository, + ) {} + + onModuleInit(): void { + this.registry.register( + { key: 'fleet.live', module: 'omega', title: 'Fleet — live vehicle positions (Cartrack, 10s cache)', description: 'Server-cached GPS position/speed/ignition per vehicle.' }, + () => this.live(), + ); + this.registry.register( + { key: 'fleet.summary', module: 'omega', title: 'Fleet — moving/stopped summary', description: 'Vehicle counts: total / moving / stopped / no-fix.' }, + () => this.summary(), + ); + this.registry.register( + { key: 'fleet.events', module: 'omega', title: 'Fleet — driver behaviour events (DB)', description: 'Harsh braking / speeding / harsh acceleration, stored from 10-min poll.' }, + async () => this.driverEvents.find({ order: { occurredAt: 'DESC' }, take: 200 }), + ); + this.registry.register( + { key: 'fleet.alerts', module: 'omega', title: 'Fleet — alerts (DB)', description: 'Geofence / ignition / panic alerts, stored from 10-min poll.' }, + async () => this.alerts.find({ where: { acknowledged: false }, order: { occurredAt: 'DESC' }, take: 100 }), + ); + this.registry.register( + { key: 'fleet.fuel', module: 'omega', title: 'Fleet — fuel level summary', description: 'Current fuel % per vehicle from 10s cache; low (<20%) flagged for CEO/VPO.' }, + async () => { + const live = await this.live(); + return live.map((v) => ({ id: v.id, registration: v.registration, fuelPct: v.fuelPct, low: v.fuelPct != null && v.fuelPct < 20 })); + }, + ); + } + + private demoFleet(): LiveVehicle[] { + const now = new Date().toISOString(); + const v = (id: string, reg: string, lat: number, lng: number, speed: number, ign: boolean, address: string): LiveVehicle => + ({ id, registration: reg, name: reg, lat, lng, speed, heading: 0, ignition: ign, odometerKm: 100_000, fuelPct: 60, driver: null, address, at: now, demo: true }); + return [ + v('D1', 'OAE-DT-114', 8.4542, 124.6319, 42, true, 'Cagayan de Oro — Daang Maharlika'), + v('D2', 'OAE-DT-118', 8.228, 124.2452, 0, false, 'Iligan — PKG-02 site yard'), + v('D3', 'OAE-SV-09', 7.1907, 125.4553, 67, true, 'Davao — Mawab-Maco bypass'), + v('D4', 'OAE-BH-21', 8.15, 125.1278, 0, false, 'Bukidnon — PKG-11 yard'), + ]; + } + + async live(): Promise { + if (!this.cartrack.configured) return this.demoFleet(); + // Serve from cache; if cache is stale (>30s, e.g. poller restart), fall back to direct call. + if (this.cache.fresh()) return this.cache.get(); + const data = await this.cartrack.get('/vehicles/status'); + const positions = this.norm.normalize(this.norm.rows(data)); + this.cache.set(positions); + return positions; + } + + async summary() { + const live = await this.live(); + return { + configured: this.cartrack.configured, + cacheAgeMs: this.cache.ageMs(), + total: live.length, + moving: live.filter((v) => v.ignition === true && (v.speed ?? 0) > 0).length, + stopped: live.filter((v) => !v.ignition || (v.speed ?? 0) === 0).length, + noFix: live.filter((v) => v.lat == null).length, + }; + } + + async locate(id: string): Promise { + if (!this.cartrack.configured) return { demo: true, message: 'set CARTRACK_USER/PASS' }; + return this.cartrack.post(`/vehicle/${id}/locate`); + } + + async vehicles(): Promise>> { + if (!this.cartrack.configured) return this.demoFleet() as unknown as Array>; + return this.norm.rows(await this.cartrack.get('/vehicles')); + } + + async trips(params: { from?: string; to?: string }): Promise>> { + if (!this.cartrack.configured) return []; + return this.norm.rows(await this.cartrack.get('/trips', { + start_timestamp: params.from ?? daysAgo(2), + end_timestamp: params.to ?? fmt(new Date()), + })); + } + + async geofences(): Promise>> { + if (!this.cartrack.configured) return []; + return this.norm.rows(await this.cartrack.get('/geofences')); + } + + async drivers(): Promise>> { + if (!this.cartrack.configured) return []; + return this.norm.rows(await this.cartrack.get('/drivers')); + } + + async maintenance(): Promise>> { + const vs = await this.vehicles(); + return vs + .map((v) => ({ + registration: v.registration, + name: v.vehicle_name ?? v.client_vehicle_description, + under_maintenance: v.is_under_maintenance ?? false, + licence_expiry: v.licence_expiry_date ?? null, + model: [v.manufacturer, v.model].filter(Boolean).join(' '), + })) + .filter((m) => m.under_maintenance || m.licence_expiry); + } + + // ── Analytics from stored data ────────────────────────────────────────── + + async positionHistory(vehicleId: string, sinceHours = 24): Promise { + const since = new Date(Date.now() - sinceHours * 3_600_000); + return this.posLog.find({ + where: { vehicleId, polledAt: LessThan(new Date(Date.now())) }, + order: { polledAt: 'ASC' }, + take: 2000, + }).then((rows) => rows.filter((r) => r.polledAt >= since)); + } + + async recentEvents(hours = 24): Promise { + const since = new Date(Date.now() - hours * 3_600_000); + return this.driverEvents.createQueryBuilder('e') + .where('e.occurred_at >= :since', { since }) + .orderBy('e.occurred_at', 'DESC') + .take(500) + .getMany(); + } + + async recentAlerts(unacknowledgedOnly = false): Promise { + const qb = this.alerts.createQueryBuilder('a').orderBy('a.occurred_at', 'DESC').take(200); + if (unacknowledgedOnly) qb.where('a.acknowledged = false'); + return qb.getMany(); + } + + async acknowledgeAlert(id: string): Promise { + await this.alerts.update(id, { acknowledged: true }); + } + + /** Fuel level trend per vehicle — most recent 48 snapshots (one per minute stored). + * Returns [{vehicleId, registration, snapshots:[{at, fuelPct}]}]. */ + async fuelTrend(hours = 8): Promise }>> { + const since = new Date(Date.now() - hours * 3_600_000); + const rows = await this.posLog + .createQueryBuilder('p') + .select(['p.vehicle_id', 'p.registration', 'p.fuel_pct', 'p.polled_at']) + .where('p.polled_at >= :since', { since }) + .orderBy('p.vehicle_id').addOrderBy('p.polled_at', 'ASC') + .getRawMany() as Array<{ p_vehicle_id: string; p_registration: string; p_fuel_pct: string | null; p_polled_at: Date }>; + + const byVehicle = new Map }>(); + for (const r of rows) { + if (!byVehicle.has(r.p_vehicle_id)) byVehicle.set(r.p_vehicle_id, { registration: r.p_registration, snapshots: [] }); + byVehicle.get(r.p_vehicle_id)!.snapshots.push({ at: r.p_polled_at, fuelPct: r.p_fuel_pct != null ? Number(r.p_fuel_pct) : null }); + } + return Array.from(byVehicle.entries()).map(([vehicleId, v]) => ({ vehicleId, ...v })); + } +} + +// ──────────────────────────── Omega equipment service ────────────────────── + +@Injectable() +export class OmegaService implements OnModuleInit { + constructor( + @InjectRepository(OmegaEquipment) private readonly equipment: Repository, + private readonly registry: ReadModelRegistry, + ) {} + + onModuleInit(): void { + this.registry.register( + { key: 'omega.equipment', module: 'omega', title: 'Omega — equipment register', description: 'All OAEC-owned units with Cartrack link, status, and project assignment.' }, + () => this.equipment.find({ order: { unitNo: 'ASC' }, take: 500 }), + ); + } + + async list(): Promise { + return this.equipment.find({ order: { unitNo: 'ASC' } }); + } + + async upsert(data: Partial, actor: string): Promise { + const existing = data.unitNo ? await this.equipment.findOne({ where: { unitNo: data.unitNo } }) : null; + const record = existing ? Object.assign(existing, data) : this.equipment.create({ ...data, entity: 'OMEGA', createdBy: actor }); + return this.equipment.save(record); + } + + async seed(actor: string): Promise { + if ((await this.equipment.count()) > 0) return; + const units = [ + { unitNo: 'OAE-DT-114', plate: 'OAE-DT-114', equipmentType: 'Dump Truck', make: 'Hino', model: '700', year: 2020, status: 'active', assignedProject: 'PKG-05' }, + { unitNo: 'OAE-DT-118', plate: 'OAE-DT-118', equipmentType: 'Dump Truck', make: 'Hino', model: '700', year: 2021, status: 'active', assignedProject: 'PKG-02' }, + { unitNo: 'OAE-SV-09', plate: 'OAE-SV-09', equipmentType: 'Service Vehicle', make: 'Toyota', model: 'Hilux', year: 2022, status: 'active', assignedProject: 'PKG-03' }, + { unitNo: 'OAE-BH-21', plate: 'OAE-BH-21', equipmentType: 'Backhoe', make: 'Komatsu', model: 'PC200', year: 2019, status: 'active', assignedProject: 'PKG-11' }, + { unitNo: 'OAE-EX-07', plate: null, equipmentType: 'Excavator', make: 'Caterpillar', model: '320', year: 2018, status: 'maintenance', assignedProject: null }, + { unitNo: 'OAE-GR-03', plate: null, equipmentType: 'Motor Grader', make: 'Caterpillar', model: '140M', year: 2017, status: 'standby', assignedProject: null }, + ]; + await this.equipment.save(units.map((u) => this.equipment.create({ ...u, entity: 'OMEGA', createdBy: actor }))); + } +} + +// ──────────────────────────── Controllers ────────────────────────────────── + +/** Fleet controller — keeps /api/fleet/... routes for web compat. */ +@Controller('fleet') +export class FleetController { + constructor(private readonly fleet: FleetService) {} + + @Get('status') + status() { return { configured: this.fleet['cartrack'].configured, region: this.fleet['cartrack'].region }; } + + @Get('summary') summary() { return this.fleet.summary(); } + + /** Live positions — served from 10s server cache, NOT direct Cartrack call. */ + @Get('live') live(): Promise { return this.fleet.live(); } + + @Get('vehicles') vehicles(): Promise { return this.fleet.vehicles(); } + @Get('geofences') geofences(): Promise { return this.fleet.geofences(); } + @Get('drivers') drivers(): Promise { return this.fleet.drivers(); } + @Get('maintenance') maintenance(): Promise { return this.fleet.maintenance(); } + + @Get('trips') + trips(@Query('from') from?: string, @Query('to') to?: string): Promise { return this.fleet.trips({ from, to }); } + + /** Events and alerts now served from DB (10-min poll), not direct Cartrack. */ + @Get('events') + events(@Query('hours') hours?: string): Promise { return this.fleet.recentEvents(Number(hours ?? 24)); } + + @Get('notifications') + alerts(@Query('unack') unack?: string): Promise { return this.fleet.recentAlerts(unack === 'true'); } + + @Post('notifications/:id/ack') + @Claims('role:manager', 'dept:operations', 'dept:engineering') + ack(@Param('id') id: string): Promise { return this.fleet.acknowledgeAlert(id); } + + @Post('vehicles/:id/locate') + locate(@Param('id') id: string): Promise { return this.fleet.locate(id); } + + /** Position history for a single vehicle (from DB). */ + @Get('vehicles/:id/history') + @Claims('role:manager', 'dept:operations', 'role:ceo', 'role:vpo') + history(@Param('id') id: string, @Query('hours') hours?: string): Promise { + return this.fleet.positionHistory(id, Number(hours ?? 24)); + } + + /** Fuel level trend for all vehicles — from stored position snapshots. */ + @Get('fuel/trend') + @Claims('role:manager', 'dept:operations', 'role:ceo', 'role:vpo') + fuelTrend(@Query('hours') hours?: string) { + return this.fleet.fuelTrend(Number(hours ?? 8)); + } +} + +/** Omega controller — equipment register at /api/omega/... */ +@Controller('omega') +export class OmegaController { + constructor(private readonly omega: OmegaService) {} + + @Get('equipment') + @Claims('role:manager', 'dept:operations', 'role:ceo', 'role:vpo') + list(): Promise { return this.omega.list(); } + + @Post('equipment') + @Claims('dept:operations', 'role:manager') + upsert(@Body() body: Partial): Promise { + return this.omega.upsert(body, 'system'); + } +} + +// ──────────────────────────── Module ─────────────────────────────────────── + +@Module({ + imports: [ + TypeOrmModule.forFeature([FleetPositionLog, FleetDriverEvent, FleetAlert, OmegaEquipment]), + AiLayerModule, + ], + controllers: [FleetController, OmegaController], + providers: [CartrackClient, FleetCache, FleetNormaliser, FleetPoller, FleetService, OmegaService], + exports: [FleetService, OmegaService, CartrackClient], +}) +export class OmegaModule {} diff --git a/apps/api/src/modules/operations/operations.module.ts b/apps/api/src/modules/operations/operations.module.ts new file mode 100644 index 0000000..2235458 --- /dev/null +++ b/apps/api/src/modules/operations/operations.module.ts @@ -0,0 +1,82 @@ +import { Controller, Get, Injectable, Module, OnModuleInit } from '@nestjs/common'; +import type { FlaggedProject } from '@ubi/types'; +import { ReadModelRegistry } from '../../pillars/ai-layer/read-model.registry'; +import { ApprovalsService } from '../../shared/approvals/approvals.service'; + +/** + * Operations = the VPO control tower: approvals inbox (shared engine), + * project health, subcon registry. First-stop rectification before the CEO. + */ +@Injectable() +export class OperationsService implements OnModuleInit { + constructor( + private readonly registry: ReadModelRegistry, + private readonly approvals: ApprovalsService, + ) {} + + onModuleInit(): void { + this.registry.register( + { + key: 'operations.health', + module: 'operations', + title: 'Project health / flagged projects', + description: 'Pre-vetted flags — the CEO sees the residue, not the noise.', + }, + () => this.flagged(), + ); + } + + /** Scaffold flags — wire to engineering slippage + SWA gaps in refine pass. */ + flagged(): FlaggedProject[] { + return [ + { + projectCode: 'PKG-05', + projectName: 'Daang Maharlika Rehab K1440', + pe: 'dela Cruz', + pm: 'Uy', + slippagePct: -4.6, + projectedPct: -6.2, + trend: [-2.1, -2.8, -3.5, -4.1, -4.6], + }, + { + projectCode: 'PKG-02', + projectName: 'Iligan Coastal Diversion', + pe: 'Ramos', + pm: 'Lim', + slippagePct: -1.2, + projectedPct: -0.8, + trend: [-1.9, -1.7, -1.6, -1.4, -1.2], + }, + { + projectCode: 'PKG-11', + projectName: 'Bukidnon Boundary Rd', + pe: 'Santos', + pm: 'Go', + slippagePct: 3.1, + projectedPct: 3.4, + trend: [1.2, 1.8, 2.2, 2.7, 3.1], + }, + ]; + } + + async summary(): Promise<{ pendingApprovals: number; flagged: FlaggedProject[] }> { + return { pendingApprovals: await this.approvals.countPending(), flagged: this.flagged() }; + } +} + +@Controller('operations') +export class OperationsController { + constructor(private readonly svc: OperationsService) {} + + @Get('health') + health(): Promise<{ pendingApprovals: number; flagged: FlaggedProject[] }> { + return this.svc.summary(); + } +} + +@Module({ + controllers: [OperationsController], + providers: [OperationsService], + exports: [OperationsService], +}) +export class OperationsModule {} diff --git a/apps/api/src/modules/procurement/procurement.module.ts b/apps/api/src/modules/procurement/procurement.module.ts new file mode 100644 index 0000000..c880af8 --- /dev/null +++ b/apps/api/src/modules/procurement/procurement.module.ts @@ -0,0 +1,191 @@ +import { + Body, + Controller, + Get, + Injectable, + Module, + OnModuleInit, + Post, +} from '@nestjs/common'; +import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import type { UserClaims } from '@ubi/types'; +import { IsNumber, IsOptional, IsString, MinLength } from 'class-validator'; +import { Column, Entity, Index, Repository } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; +import { CurrentUser } from '../../common/rbac'; +import { ReadModelRegistry } from '../../pillars/ai-layer/read-model.registry'; +import { ApprovalsService } from '../../shared/approvals/approvals.service'; +import { MaterialsScheduleEntity, ProjectEntity } from '../engineering/entities'; +import { EngineeringModule } from '../engineering/engineering.module'; + +/** + * DR receiving — OAEC/ZeroType pattern: site inspector captures photo + + * details on the edge; an OFF-BOX agent encodes into Acumatica later. + * Postgres keeps metadata + the file id only. + */ +@Entity('delivery_receipt') +export class DeliveryReceiptEntity extends BaseAppEntity { + @Index() + @Column({ name: 'project_id' }) + projectId: string; + + @Column() + supplier: string; + + @Column({ name: 'dr_no' }) + drNo: string; + + @Column({ type: 'text' }) + items: string; + + @Column({ type: 'varchar', nullable: true }) + gps: string | null; + + /** FileEntity id of the compressed (<500 KB) DR photo. */ + @Column({ name: 'photo_file_id', type: 'varchar', nullable: true }) + photoFileId: string | null; + + /** Set by the off-box AI-encode agent once pushed to Acumatica. */ + @Column({ name: 'acumatica_ref', type: 'varchar', nullable: true }) + acumaticaRef: string | null; + + /** Cement deliveries are flagged for monitoring (receiver role TBD). */ + @Column({ name: 'is_cement', default: false }) + isCement: boolean; +} + +class CaptureDrDto { + @IsString() projectId: string; + @IsString() @MinLength(2) supplier: string; + @IsString() @MinLength(1) drNo: string; + @IsString() @MinLength(2) items: string; + @IsOptional() @IsString() gps?: string; + @IsOptional() @IsString() photoFileId?: string; + @IsOptional() @IsNumber() lateDays?: number; +} + +@Injectable() +export class ProcurementService implements OnModuleInit { + constructor( + @InjectRepository(DeliveryReceiptEntity) private readonly drs: Repository, + @InjectRepository(MaterialsScheduleEntity) private readonly schedules: Repository, + @InjectRepository(ProjectEntity) private readonly projects: Repository, + private readonly approvals: ApprovalsService, + private readonly registry: ReadModelRegistry, + ) {} + + onModuleInit(): void { + this.registry.register( + { + key: 'procurement.dr-log', + module: 'procurement', + title: 'Delivery receipts (edge-captured)', + description: 'DR photo captures awaiting/done AI-encode to Acumatica.', + }, + () => this.drs.find({ order: { createdAt: 'DESC' }, take: 100 }), + ); + this.registry.register( + { + key: 'procurement.supplier-performance', + module: 'procurement', + title: 'Supplier delivery performance', + description: 'Derived from receiving data — late days per supplier.', + }, + () => this.supplierPerformance(), + ); + this.registry.register( + { + key: 'procurement.materials-intake', + module: 'procurement', + title: 'Incoming materials schedules', + description: 'Approved (PE→PM→VPO) weekly materials schedules arriving from Engineering.', + }, + () => this.materialsIntake(), + ); + } + + /** Materials schedules with their approval status — Procurement's inbox. */ + async materialsIntake(): Promise< + Array<{ id: string; projectCode: string; weekOf: string; lines: number; approval: string; status: string }> + > { + const [schedules, projects] = await Promise.all([ + this.schedules.find({ order: { createdAt: 'DESC' }, take: 100 }), + this.projects.find(), + ]); + const byId = new Map(projects.map((p) => [p.id, p.code] as const)); + const out = []; + for (const s of schedules) { + const appr = await this.approvals.forSubject('materials-schedule', s.id); + const approval = appr?.status ?? 'none'; + out.push({ + id: s.id, + projectCode: byId.get(s.projectId) ?? s.projectId, + weekOf: s.weekOf, + lines: s.lines.length, + approval, + // Procurement acts only on fully-approved schedules. + status: approval === 'approved' ? 'ready-to-buy' : approval === 'returned' ? 'returned' : 'awaiting-approval', + }); + } + return out; + } + + capture(user: UserClaims, dto: CaptureDrDto): Promise { + return this.drs.save( + this.drs.create({ + projectId: dto.projectId, + supplier: dto.supplier, + drNo: dto.drNo, + items: dto.items, + gps: dto.gps ?? null, + photoFileId: dto.photoFileId ?? null, + isCement: /cement/i.test(dto.items), + entity: user.entity, + createdBy: user.email, + }), + ); + } + + /** Scaffold scoring — replace with real lateness calc when PO data syncs. */ + async supplierPerformance(): Promise> { + const all = await this.drs.find(); + const bySupplier = new Map(); + for (const dr of all) bySupplier.set(dr.supplier, (bySupplier.get(dr.supplier) ?? 0) + 1); + return [...bySupplier.entries()].map(([supplier, deliveries]) => ({ + supplier, + deliveries, + avgLateDays: supplier === 'Solid Mix Corp' ? 2.4 : 0.3, + })); + } +} + +@Controller('procurement') +export class ProcurementController { + constructor(private readonly svc: ProcurementService) {} + + @Post('dr') + capture(@CurrentUser() user: UserClaims, @Body() dto: CaptureDrDto): Promise { + return this.svc.capture(user, dto); + } + + @Get('supplier-performance') + performance(): Promise> { + return this.svc.supplierPerformance(); + } + + @Get('materials-intake') + intake(): ReturnType { + return this.svc.materialsIntake(); + } +} + +@Module({ + imports: [ + TypeOrmModule.forFeature([DeliveryReceiptEntity, MaterialsScheduleEntity, ProjectEntity]), + EngineeringModule, + ], + controllers: [ProcurementController], + providers: [ProcurementService], + exports: [ProcurementService], +}) +export class ProcurementModule {} diff --git a/apps/api/src/modules/property/property.module.ts b/apps/api/src/modules/property/property.module.ts new file mode 100644 index 0000000..0614283 --- /dev/null +++ b/apps/api/src/modules/property/property.module.ts @@ -0,0 +1,278 @@ +import { + Body, + Controller, + Get, + Injectable, + Module, + NotFoundException, + OnModuleInit, + Param, + Post, +} from '@nestjs/common'; +import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import type { UserClaims } from '@ubi/types'; +import { IsIn, IsOptional, IsString, MinLength } from 'class-validator'; +import { Column, Entity, Index, Repository } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; +import { CurrentUser } from '../../common/rbac'; +import { ReadModelRegistry } from '../../pillars/ai-layer/read-model.registry'; + +export type AssetStatus = 'in-use' | 'idle' | 'maintenance' | 'retired'; + +/** QR-based lightweight EAM — who holds what, where, in what state. */ +@Entity('asset') +export class AssetEntity extends BaseAppEntity { + @Index({ unique: true }) + @Column() + qr: string; + + @Column() + type: string; + + /** Coarse category for PMS selection, e.g. "Heavy equipment", "IT equipment". */ + @Column({ default: 'General' }) + category: string; + + @Column({ type: 'varchar', default: 'idle' }) + status: AssetStatus; + + @Column() + location: string; + + @Column() + custodian: string; + + @Column({ name: 'custodian_email', type: 'varchar', nullable: true }) + custodianEmail: string | null; + + /** Active assets feed the IT PMS scheduler (quarterly maintenance). */ + @Column({ name: 'pms_eligible', default: true }) + pmsEligible: boolean; +} + +@Entity('asset_movement') +export class AssetMovementEntity extends BaseAppEntity { + @Index() + @Column({ name: 'asset_id' }) + assetId: string; + + @Column({ type: 'varchar' }) + kind: 'scan' | 'issue' | 'return' | 'status' | 'create'; + + @Column({ type: 'varchar', nullable: true }) + fromCustodian: string | null; + + @Column({ type: 'varchar', nullable: true }) + toCustodian: string | null; + + @Column() + location: string; + + @Column({ type: 'varchar' }) + status: AssetStatus; + + @Column() + by: string; + + @Column({ type: 'varchar', nullable: true }) + note: string | null; +} + +/** Monthly custodian attestation — each custodian confirms what they hold. */ +@Entity('custodian_attestation') +export class CustodianAttestationEntity extends BaseAppEntity { + @Index() + @Column({ name: 'asset_id' }) + assetId: string; + + @Column() + custodian: string; + + /** YYYY-MM */ + @Column() + month: string; + + @Column({ default: true }) + confirmed: boolean; + + @Column({ type: 'varchar', nullable: true }) + note: string | null; + + @Column() + by: string; +} + +class ScanDto { + @IsOptional() @IsString() location?: string; + @IsOptional() @IsIn(['in-use', 'idle', 'maintenance', 'retired']) status?: AssetStatus; + @IsOptional() @IsString() custodian?: string; + @IsOptional() @IsString() note?: string; +} + +class AttestDto { + @IsString() @MinLength(7) month: string; + @IsOptional() @IsString() note?: string; +} + +@Injectable() +export class PropertyService implements OnModuleInit { + constructor( + @InjectRepository(AssetEntity) private readonly assets: Repository, + @InjectRepository(AssetMovementEntity) private readonly moves: Repository, + @InjectRepository(CustodianAttestationEntity) private readonly attests: Repository, + private readonly registry: ReadModelRegistry, + ) {} + + onModuleInit(): void { + this.registry.register( + { key: 'property.assets', module: 'property', title: 'Asset custody (QR EAM)', description: 'Who holds what; QR scans update location/status/custodian.' }, + () => this.assets.find({ order: { qr: 'ASC' } }), + ); + this.registry.register( + { key: 'property.attestations', module: 'property', title: 'Monthly custodian attestations', description: 'Each custodian confirms equipment issued to them — audit trail.' }, + () => this.attests.find({ order: { createdAt: 'DESC' }, take: 100 }), + ); + } + + list(): Promise { + return this.assets.find({ order: { qr: 'ASC' } }); + } + + async byQr(qr: string): Promise { + const a = await this.assets.findOne({ where: { qr } }); + if (!a) throw new NotFoundException(`no asset ${qr}`); + return a; + } + + async history(assetId: string): Promise { + return this.moves.find({ where: { assetId }, order: { createdAt: 'DESC' } }); + } + + /** Mobile scans a QR → log location/status/custodian change. */ + async scan(user: UserClaims, qr: string, dto: ScanDto): Promise { + const a = await this.byQr(qr); + const fromCustodian = a.custodian; + if (dto.location) a.location = dto.location; + if (dto.status) a.status = dto.status; + if (dto.custodian) { + a.custodian = dto.custodian; + a.custodianEmail = null; + } + const saved = await this.assets.save(a); + await this.moves.save( + this.moves.create({ + assetId: a.id, + kind: dto.custodian ? 'issue' : 'scan', + fromCustodian, + toCustodian: dto.custodian ?? fromCustodian, + location: a.location, + status: a.status, + by: user.email, + note: dto.note ?? null, + entity: a.entity, + createdBy: user.email, + }), + ); + return saved; + } + + async attest(user: UserClaims, qr: string, dto: AttestDto): Promise { + const a = await this.byQr(qr); + return this.attests.save( + this.attests.create({ + assetId: a.id, + custodian: a.custodian, + month: dto.month, + confirmed: true, + note: dto.note ?? null, + by: user.email, + entity: a.entity, + createdBy: user.email, + }), + ); + } + + attestations(): Promise { + return this.attests.find({ order: { createdAt: 'DESC' }, take: 100 }); + } + + /** Active, PMS-eligible assets — consumed by the IT PMS scheduler. */ + pmsEligible(): Promise { + return this.assets.find({ where: { pmsEligible: true } }); + } + + /** Distinct categories among PMS-eligible assets (for the scheduler picker). */ + async categories(): Promise> { + const rows = await this.assets.find({ where: { pmsEligible: true } }); + const map = new Map(); + for (const a of rows) map.set(a.category, (map.get(a.category) ?? 0) + 1); + return [...map.entries()].map(([category, count]) => ({ category, count })).sort((a, b) => a.category.localeCompare(b.category)); + } + + pmsEligibleByCategories(categories: string[]): Promise { + return this.assets.find({ where: { pmsEligible: true } }).then((rows) => + categories.length ? rows.filter((a) => categories.includes(a.category)) : rows, + ); + } + + async seed(creator: string): Promise { + if ((await this.assets.count()) > 0) return; + const rows: Array> = [ + { qr: 'AST-00112', type: 'Total station (Leica TS07)', category: 'Survey equipment', status: 'in-use', location: 'PKG-02 site office', custodian: 'Survey — Reyes' }, + { qr: 'AST-00387', type: 'Plate compactor', category: 'Heavy equipment', status: 'in-use', location: 'PKG-05', custodian: 'PKG-05 warehouse' }, + { qr: 'AST-00421', type: 'Laptop (Dell 5440)', category: 'IT equipment', status: 'in-use', location: 'HQ 3F Engineering', custodian: 'Engr. dela Cruz' }, + { qr: 'AST-00455', type: 'Concrete vibrator', category: 'Heavy equipment', status: 'maintenance', location: 'HQ motor pool', custodian: 'Property warehouse' }, + { qr: 'AST-00501', type: 'Generator 10kVA', category: 'Power equipment', status: 'idle', location: 'PKG-11 yard', custodian: 'PKG-11 warehouse' }, + { qr: 'AST-00540', type: 'Desktop + UPS (admin)', category: 'IT equipment', status: 'in-use', location: 'HQ 2F Admin', custodian: 'Admin — Flores' }, + { qr: 'AST-00566', type: 'Backhoe loader', category: 'Heavy equipment', status: 'in-use', location: 'PKG-05', custodian: 'PKG-05 motor pool' }, + ]; + for (const r of rows) { + const a = await this.assets.save(this.assets.create({ ...r, createdBy: creator })); + await this.moves.save( + this.moves.create({ + assetId: a.id, kind: 'create', fromCustodian: null, toCustodian: a.custodian, + location: a.location, status: a.status, by: creator, note: 'labeled + registered', createdBy: creator, + }), + ); + } + } +} + +@Controller('property') +export class PropertyController { + constructor(private readonly svc: PropertyService) {} + + @Get('assets') + list(): Promise { + return this.svc.list(); + } + + @Get('assets/:qr') + async one(@Param('qr') qr: string): Promise<{ asset: AssetEntity; history: AssetMovementEntity[] }> { + const asset = await this.svc.byQr(qr); + return { asset, history: await this.svc.history(asset.id) }; + } + + @Post('assets/:qr/scan') + scan(@CurrentUser() user: UserClaims, @Param('qr') qr: string, @Body() dto: ScanDto): Promise { + return this.svc.scan(user, qr, dto); + } + + @Post('assets/:qr/attest') + attest(@CurrentUser() user: UserClaims, @Param('qr') qr: string, @Body() dto: AttestDto): Promise { + return this.svc.attest(user, qr, dto); + } + + @Get('attestations') + attestations(): Promise { + return this.svc.attestations(); + } +} + +@Module({ + imports: [TypeOrmModule.forFeature([AssetEntity, AssetMovementEntity, CustodianAttestationEntity])], + controllers: [PropertyController], + providers: [PropertyService], + exports: [PropertyService], +}) +export class PropertyModule {} diff --git a/apps/api/src/modules/quantity/quantity.module.ts b/apps/api/src/modules/quantity/quantity.module.ts new file mode 100644 index 0000000..30f4441 --- /dev/null +++ b/apps/api/src/modules/quantity/quantity.module.ts @@ -0,0 +1,235 @@ +import { Body, Controller, Get, Injectable, Module, OnModuleInit, Post, Query } from '@nestjs/common'; +import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import type { QuantityRollup, UserClaims } from '@ubi/types'; +import { IsNumber as _N, IsOptional, IsString, MinLength } from 'class-validator'; +import { Column, Entity, Index, Repository } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; +import { CurrentUser } from '../../common/rbac'; +import { ReadModelRegistry } from '../../pillars/ai-layer/read-model.registry'; +import { ProjectEntity } from '../engineering/entities'; + +/** Weekly accomplishment by a QE onsite — auto-rolls into the monthly report. */ +@Entity('quantity_entry') +export class QuantityEntryEntity extends BaseAppEntity { + @Index() @Column({ name: 'project_id' }) projectId: string; + @Column({ name: 'week_of', type: 'date' }) weekOf: string; + @Column({ type: 'double precision' }) amount: number; + @Column({ name: 'entered_by' }) enteredBy: string; + @Column({ type: 'varchar', nullable: true }) note: string | null; +} + +/** Monthly projected ₱ accomplishment (gdrive replacement, top row). */ +@Entity('month_projection') +export class MonthProjectionEntity extends BaseAppEntity { + @Index() @Column({ name: 'project_id' }) projectId: string; + @Column() month: string; // YYYY-MM + @Column({ name: 'projected_amount', type: 'double precision', default: 0 }) projectedAmount: number; +} + +/** Monthly material requirement per project (gdrive replacement, material rows). */ +@Entity('material_req') +export class MaterialReqEntity extends BaseAppEntity { + @Index() @Column({ name: 'project_id' }) projectId: string; + @Column() month: string; + @Column() material: string; + @Column() unit: string; + @Column({ type: 'double precision', default: 0 }) qty: number; +} + +class EntryDto { + @IsString() projectId: string; + @IsString() weekOf: string; + @_N() amount: number; + @IsOptional() @IsString() note?: string; +} +class ProjectionDto { + @IsString() projectId: string; + @IsString() @MinLength(7) month: string; + @_N() projectedAmount: number; +} +class MaterialDto { + @IsString() projectId: string; + @IsString() @MinLength(7) month: string; + @IsString() material: string; + @IsString() unit: string; + @_N() qty: number; +} + +@Injectable() +export class QuantityService implements OnModuleInit { + constructor( + @InjectRepository(QuantityEntryEntity) private readonly entries: Repository, + @InjectRepository(MonthProjectionEntity) private readonly projections: Repository, + @InjectRepository(MaterialReqEntity) private readonly materials: Repository, + @InjectRepository(ProjectEntity) private readonly projects: Repository, + private readonly registry: ReadModelRegistry, + ) {} + + onModuleInit(): void { + this.registry.register( + { key: 'engineering.quantity', module: 'engineering', title: 'Quantity — accomplishment rollup', description: 'Weekly QE entries rolled to per-project to-date %, this-month, slippage.' }, + () => this.rollup(), + ); + this.registry.register( + { key: 'engineering.material-reqs', module: 'engineering', title: 'Monthly material requirements', description: 'Projected materials per project per month — feeds Procurement.' }, + () => this.materials.find({ order: { month: 'ASC' }, take: 500 }), + ); + } + + private ym(d: Date): string { + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; + } + + async rollup(): Promise { + const projects = await this.projects.find({ order: { code: 'ASC' } }); + const allEntries = await this.entries.find(); + const thisMonth = this.ym(new Date()); + return projects.map((p) => { + const mine = allEntries.filter((e) => e.projectId === p.id); + const sum = mine.reduce((s, e) => s + Number(e.amount), 0); + const toDate = Number(p.priorAccomplished) + sum; + const monthSum = mine.filter((e) => this.ym(new Date(e.weekOf)) === thisMonth).reduce((s, e) => s + Number(e.amount), 0); + const last = mine.sort((a, b) => b.weekOf.localeCompare(a.weekOf))[0]; + const inhouse = Number(p.inhouseAmount) || 0; + return { + projectId: p.id, + code: p.code, + name: p.name, + pe: p.pe, + inhouseAmount: inhouse, + priorAccomplished: Number(p.priorAccomplished), + toDate, + pct: inhouse ? toDate / inhouse : 0, + thisMonth: monthSum, + lastWeekOf: last?.weekOf ?? null, + entity: p.entity, + }; + }); + } + + async headSummary(): Promise<{ + projects: number; + totalInhouse: number; + totalToDate: number; + overallPct: number; + thisMonth: number; + behind: number; + rows: QuantityRollup[]; + }> { + const rows = await this.rollup(); + const totalInhouse = rows.reduce((s, r) => s + r.inhouseAmount, 0); + const totalToDate = rows.reduce((s, r) => s + r.toDate, 0); + const thisMonth = rows.reduce((s, r) => s + r.thisMonth, 0); + return { + projects: rows.length, + totalInhouse, + totalToDate, + overallPct: totalInhouse ? totalToDate / totalInhouse : 0, + thisMonth, + behind: rows.filter((r) => r.pct < 0.85).length, + rows, + }; + } + + addEntry(user: UserClaims, dto: EntryDto): Promise { + return this.entries.save( + this.entries.create({ ...dto, note: dto.note ?? null, enteredBy: user.email, entity: user.entity, createdBy: user.email }), + ); + } + + entriesFor(projectId: string): Promise { + return this.entries.find({ where: { projectId }, order: { weekOf: 'DESC' }, take: 60 }); + } + + /** Upsert a month's projected ₱ accomplishment. */ + async setProjection(user: UserClaims, dto: ProjectionDto): Promise { + const found = await this.projections.findOne({ where: { projectId: dto.projectId, month: dto.month } }); + if (found) { + found.projectedAmount = dto.projectedAmount; + return this.projections.save(found); + } + return this.projections.save(this.projections.create({ ...dto, entity: user.entity, createdBy: user.email })); + } + + projectionsFor(projectId: string): Promise { + return this.projections.find({ where: { projectId }, order: { month: 'ASC' } }); + } + + /** Upsert a material qty for a project-month. */ + async setMaterial(user: UserClaims, dto: MaterialDto): Promise { + const found = await this.materials.findOne({ where: { projectId: dto.projectId, month: dto.month, material: dto.material } }); + if (found) { + found.qty = dto.qty; + found.unit = dto.unit; + return this.materials.save(found); + } + return this.materials.save(this.materials.create({ ...dto, entity: user.entity, createdBy: user.email })); + } + + materialsFor(projectId: string): Promise { + return this.materials.find({ where: { projectId }, order: { month: 'ASC' } }); + } + + async seed(): Promise { + if ((await this.entries.count()) > 0) return; + const projects = await this.projects.find(); + // Backfill in-house amounts + PE + prior accomplishment + a few weekly entries. + const profile: Record = { + 'PKG-02': { inhouse: 214435919, pe: 'J. Labadia', prior: 150000000, weeks: [3439516, 2890000, 3120000] }, + 'PKG-05': { inhouse: 169137006, pe: 'J. Paraso', prior: 96000000, weeks: [4100000, 3800000, 2950000] }, + 'PKG-11': { inhouse: 44427777, pe: 'R. Santos', prior: 24280766, weeks: [2100000, 1980000, 2160000] }, + 'PKG-03': { inhouse: 86807306, pe: 'M. Dela Cruz', prior: 60000000, weeks: [1800000, 1500000] }, + 'PKG-07': { inhouse: 97329310, pe: 'E. Devora', prior: 71000000, weeks: [1200000, 900000] }, + 'PKG-09': { inhouse: 120000000, pe: 'B. Gutierrez', prior: 30000000, weeks: [3000000, 2500000] }, + }; + for (const p of projects) { + const prof = profile[p.code]; + if (!prof) continue; + p.inhouseAmount = prof.inhouse; + p.pe = prof.pe; + p.priorAccomplished = prof.prior; + await this.projects.save(p); + const today = new Date(); + for (let i = 0; i < prof.weeks.length; i++) { + const d = new Date(today); + d.setDate(today.getDate() - 7 * (prof.weeks.length - i)); + await this.entries.save( + this.entries.create({ projectId: p.id, weekOf: d.toISOString().slice(0, 10), amount: prof.weeks[i], enteredBy: 'seed', createdBy: 'seed' }), + ); + } + // a forward projection + a couple material lines for the next 3 months + const months = [0, 1, 2].map((k) => { + const d = new Date(today.getFullYear(), today.getMonth() + 1 + k, 1); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; + }); + for (const m of months) { + await this.projections.save(this.projections.create({ projectId: p.id, month: m, projectedAmount: prof.weeks[0] * 4, createdBy: 'seed' })); + await this.materials.save(this.materials.create({ projectId: p.id, month: m, material: 'Cement Tonner', unit: 'tonner bags', qty: 120, createdBy: 'seed' })); + await this.materials.save(this.materials.create({ projectId: p.id, month: m, material: 'Item 200', unit: 'cu.m', qty: 200, createdBy: 'seed' })); + } + } + } +} + +@Controller('quantity') +export class QuantityController { + constructor(private readonly svc: QuantityService) {} + + @Get('rollup') rollup(): Promise { return this.svc.rollup(); } + @Get('head') head(): ReturnType { return this.svc.headSummary(); } + @Get('entries') entries(@Query('projectId') id: string): Promise { return this.svc.entriesFor(id); } + @Post('entry') addEntry(@CurrentUser() u: UserClaims, @Body() dto: EntryDto): Promise { return this.svc.addEntry(u, dto); } + + @Get('projections') projections(@Query('projectId') id: string): Promise { return this.svc.projectionsFor(id); } + @Post('projection') setProjection(@CurrentUser() u: UserClaims, @Body() dto: ProjectionDto): Promise { return this.svc.setProjection(u, dto); } + @Get('materials') materials(@Query('projectId') id: string): Promise { return this.svc.materialsFor(id); } + @Post('material') setMaterial(@CurrentUser() u: UserClaims, @Body() dto: MaterialDto): Promise { return this.svc.setMaterial(u, dto); } +} + +@Module({ + imports: [TypeOrmModule.forFeature([QuantityEntryEntity, MonthProjectionEntity, MaterialReqEntity, ProjectEntity])], + controllers: [QuantityController], + providers: [QuantityService], + exports: [QuantityService], +}) +export class QuantityModule {} diff --git a/apps/api/src/modules/records/records.module.ts b/apps/api/src/modules/records/records.module.ts new file mode 100644 index 0000000..f6f90aa --- /dev/null +++ b/apps/api/src/modules/records/records.module.ts @@ -0,0 +1,228 @@ +import { + Body, + Controller, + ForbiddenException, + Get, + Injectable, + Module, + OnModuleInit, + Post, +} from '@nestjs/common'; +import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import type { UserClaims } from '@ubi/types'; +import { IsDateString, IsIn, IsOptional, IsString, MinLength } from 'class-validator'; +import { Column, Entity, Index, Repository } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; +import { CurrentUser, hasClaim } from '../../common/rbac'; +import { ReadModelRegistry } from '../../pillars/ai-layer/read-model.registry'; +import { DocTrackingModule } from '../../pillars/doc-tracking/doc-tracking.module'; +import { DocTrackingService } from '../../pillars/doc-tracking/doc-tracking.service'; +import { ExpiryService } from '../../shared/expiry/expiry.module'; + +/** Vehicle registration / insurance / stamps with expiry. */ +@Entity('vehicle_doc') +export class VehicleDocEntity extends BaseAppEntity { + @Index() + @Column() + plate: string; + + @Column({ type: 'varchar' }) + kind: 'registration' | 'insurance' | 'stamp'; + + @Column({ name: 'ref_no' }) + refNo: string; + + @Column({ name: 'expires_on', type: 'date' }) + expiresOn: string; +} + +/** Physical location index — HARD-GATED: sensitive docs (titles) only for + * authorized roles. AI-searchable but access-controlled (spec). */ +@Entity('physical_location') +export class PhysicalLocationEntity extends BaseAppEntity { + @Column() + title: string; + + @Column() + cabinet: string; + + @Column() + drawer: string; + + /** Claim required to view, e.g. 'dept:records' or 'role:legal'. */ + @Column({ name: 'requires_claim', type: 'varchar', default: 'dept:records' }) + requiresClaim: string; + + @Column({ default: false }) + sensitive: boolean; +} + +class VehicleDocDto { + @IsString() @MinLength(2) plate: string; + @IsIn(['registration', 'insurance', 'stamp']) kind: 'registration' | 'insurance' | 'stamp'; + @IsString() @MinLength(2) refNo: string; + @IsDateString() expiresOn: string; +} + +class LocationDto { + @IsString() @MinLength(2) title: string; + @IsString() @MinLength(1) cabinet: string; + @IsString() @MinLength(1) drawer: string; + @IsOptional() @IsString() requiresClaim?: string; + @IsOptional() sensitive?: boolean; +} + +@Injectable() +export class RecordsService implements OnModuleInit { + constructor( + @InjectRepository(VehicleDocEntity) private readonly vdocs: Repository, + @InjectRepository(PhysicalLocationEntity) private readonly locs: Repository, + private readonly expiry: ExpiryService, + private readonly docs: DocTrackingService, + private readonly registry: ReadModelRegistry, + ) {} + + onModuleInit(): void { + // Records is the home of Document Tracking (Pillar 2) — it monitors the + // company-wide document registry (Finance is only a consumer). + this.registry.register( + { key: 'records.documents', module: 'records', title: 'Document registry & movement', description: 'Every tracked document: holder, location, status, history.' }, + () => this.docs.list(), + ); + this.registry.register( + { key: 'records.expiry', module: 'records', title: 'Expiring documents', description: 'Vehicle reg/insurance/stamps + tracked items within 30 days.' }, + () => this.upcoming(), + ); + this.registry.register( + { + key: 'records.physical-index', + module: 'records', + title: 'Physical location index', + description: 'Cabinet/drawer index. Sensitive rows hidden unless caller holds the required claim.', + requiresClaim: 'dept:records', + }, + (user) => this.indexFor(user), + ); + } + + documents(): ReturnType { + return this.docs.list(); + } + + async upcoming(): Promise> { + const now = Date.now(); + const fromVehicles = (await this.vdocs.find()).map((v) => ({ + title: `${v.plate} ${v.kind} (${v.refNo})`, + kind: v.kind, + expiresOn: v.expiresOn, + daysLeft: Math.ceil((new Date(v.expiresOn).getTime() - now) / 86_400_000), + })); + const fromEngine = (await this.expiry.upcoming(30)).map((e) => ({ + title: e.title, + kind: e.kind, + expiresOn: e.expiresOn, + daysLeft: Math.ceil((new Date(e.expiresOn).getTime() - now) / 86_400_000), + })); + return [...fromVehicles, ...fromEngine] + .filter((r) => r.daysLeft <= 60) + .sort((a, b) => a.daysLeft - b.daysLeft); + } + + async addVehicleDoc(user: UserClaims, dto: VehicleDocDto): Promise { + const saved = await this.vdocs.save(this.vdocs.create({ ...dto, entity: user.entity, createdBy: user.email })); + // mirror into the shared expiry/reminder engine + await this.expiry.add({ + kind: `vehicle-${dto.kind}`, + refModule: 'records', + title: `${dto.plate} ${dto.kind}`, + expiresOn: dto.expiresOn, + notifyAudience: 'dept:records', + }); + return saved; + } + + /** Physical index, filtered by what the caller is allowed to see. */ + async indexFor(user: UserClaims): Promise>> { + const all = await this.locs.find({ order: { cabinet: 'ASC' } }); + return all.map((l) => { + const allowed = hasClaim(user, l.requiresClaim); + return { + title: allowed ? l.title : '🔒 restricted', + cabinet: allowed ? l.cabinet : '—', + drawer: allowed ? l.drawer : '—', + sensitive: l.sensitive, + access: allowed ? 'visible' : `needs ${l.requiresClaim}`, + }; + }); + } + + async addLocation(user: UserClaims, dto: LocationDto): Promise { + if (!hasClaim(user, 'dept:records')) throw new ForbiddenException('records staff only'); + return this.locs.save( + this.locs.create({ + title: dto.title, + cabinet: dto.cabinet, + drawer: dto.drawer, + requiresClaim: dto.requiresClaim ?? 'dept:records', + sensitive: dto.sensitive ?? false, + entity: user.entity, + createdBy: user.email, + }), + ); + } + + async seed(creator: string): Promise { + if ((await this.vdocs.count()) === 0) { + await this.vdocs.save([ + this.vdocs.create({ plate: 'UBI-DT-114', kind: 'registration', refNo: 'LTO-2025-88421', expiresOn: '2026-06-24', createdBy: creator }), + this.vdocs.create({ plate: 'UBI-DT-118', kind: 'registration', refNo: 'LTO-2025-88450', expiresOn: '2026-06-27', createdBy: creator }), + this.vdocs.create({ plate: 'UBI-SV-09', kind: 'insurance', refNo: 'MAA-INS-7741', expiresOn: '2026-06-22', createdBy: creator }), + ]); + } + if ((await this.locs.count()) === 0) { + await this.locs.save([ + this.locs.create({ title: 'PKG-02 contract & NTP', cabinet: 'Cabinet 1', drawer: 'Drawer A', requiresClaim: 'dept:records', sensitive: false, createdBy: creator }), + this.locs.create({ title: 'Land Title — HQ lot (TCT-44512)', cabinet: 'Steel Cabinet 3', drawer: 'Drawer B', requiresClaim: 'role:legal', sensitive: true, createdBy: creator }), + this.locs.create({ title: 'Company SEC registration', cabinet: 'Cabinet 1', drawer: 'Drawer C', requiresClaim: 'dept:records', sensitive: false, createdBy: creator }), + ]); + } + } +} + +@Controller('records') +export class RecordsController { + constructor(private readonly svc: RecordsService) {} + + @Get('documents') + documents(): Promise { + return this.svc.documents(); + } + + @Get('expiry') + expiry(): Promise { + return this.svc.upcoming(); + } + + @Post('vehicle-docs') + addVehicle(@CurrentUser() user: UserClaims, @Body() dto: VehicleDocDto): Promise { + return this.svc.addVehicleDoc(user, dto); + } + + @Get('physical-index') + index(@CurrentUser() user: UserClaims): Promise { + return this.svc.indexFor(user); + } + + @Post('physical-index') + addLocation(@CurrentUser() user: UserClaims, @Body() dto: LocationDto): Promise { + return this.svc.addLocation(user, dto); + } +} + +@Module({ + imports: [TypeOrmModule.forFeature([VehicleDocEntity, PhysicalLocationEntity]), DocTrackingModule], + controllers: [RecordsController], + providers: [RecordsService], + exports: [RecordsService], +}) +export class RecordsModule {} diff --git a/apps/api/src/modules/stubs/stubs.module.ts b/apps/api/src/modules/stubs/stubs.module.ts new file mode 100644 index 0000000..98a44ce --- /dev/null +++ b/apps/api/src/modules/stubs/stubs.module.ts @@ -0,0 +1,135 @@ +import { Controller, Get, Injectable, Module, OnModuleInit } from '@nestjs/common'; +import type { ReportStatus } from '@ubi/types'; +import { ReadModelRegistry } from '../../pillars/ai-layer/read-model.registry'; +import { DocTrackingService } from '../../pillars/doc-tracking/doc-tracking.service'; +import { DocTrackingModule } from '../../pillars/doc-tracking/doc-tracking.module'; +import { ExpiryService } from '../../shared/expiry/expiry.module'; +import { TicketingService } from '../../shared/ticketing/ticketing.module'; + +/** + * Breadth-first stubs for Survey, MQC, Audit, IT, Records, Clinic, Admin, + * HR, Property and Finance: each registers its Pillar-3 read surface now so + * the contract exists from day one; deep workflows land on their module + * branches (survey, mqc, audit, it, records, clinic, admin, hr, property, + * finance) in later sessions. + */ +@Injectable() +export class StubReadModelsService implements OnModuleInit { + constructor( + private readonly registry: ReadModelRegistry, + private readonly expiry: ExpiryService, + private readonly ticketing: TicketingService, + private readonly docs: DocTrackingService, + ) {} + + onModuleInit(): void { + // survey.volumes and mqc.certs are now owned by SurveyModule and MqcModule. + + this.registry.register( + { + key: 'audit.exceptions', + module: 'audit', + title: 'Exception / investigation register', + description: 'Raised → investigating → resolved/escalated, full trail.', + }, + () => [ + { id: 'EXC-031', title: 'Fuel variance PKG-05 haul fleet', status: 'escalated', raised: '2026-05-28' }, + { id: 'EXC-034', title: 'Unmatched DR vs PO — aggregates', status: 'escalated', raised: '2026-06-02' }, + { id: 'EXC-035', title: 'PPE issuance gap, site 11', status: 'investigating', raised: '2026-06-08' }, + ], + ); + + // NOTE: it.capacity, records.expiry and property.assets are now owned by + // the real ItModule / RecordsModule / PropertyModule. + + this.registry.register( + { + key: 'clinic.aggregate', + module: 'clinic', + title: 'Clinic aggregate health summary', + description: 'AGGREGATE ONLY — individual medical records never leave the isolated clinic store.', + }, + () => ({ + month: '2026-06', + medicalExamCompletionPct: 92, + clinicVisits: 138, + injuries: 2, + doleReportsOnTime: true, + }), + ); + + this.registry.register( + { + key: 'admin.tickets', + module: 'admin', + title: 'Admin inquiries (shared ticketing)', + description: 'Site inquiries/requests routed through the shared engine.', + }, + () => this.ticketing.list('admin'), + ); + + this.registry.register( + { + key: 'hr.hours', + module: 'hr', + title: 'Hours computation runs', + description: 'OT/ND totals computed from attendance, pushed to Ezacctg.', + }, + () => [{ run: '2026-06-A', period: 'Jun 1–15', status: 'computed', pushedToEzacctg: false, employees: 487 }], + ); + + this.registry.register( + { + key: 'finance.doc-flow', + module: 'finance', + title: 'Documents with Finance (batches to cashier)', + description: "Finance is a consumer of Document Tracking — only the docs it currently holds or is batching. The registry itself is monitored by Records.", + }, + async () => { + const docs = await this.docs.list(); + return docs.filter( + (d) => d.currentHolder.toLowerCase().includes('finance') || d.currentLocation.toLowerCase().includes('cashier'), + ); + }, + ); + } +} + +/** Per-department summary endpoints, all answering from the registry. */ +@Controller() +export class StubsController { + constructor(private readonly registry: ReadModelRegistry) {} + + private query(key: string): unknown { + const model = this.registry.get(key); + return model + ? model.handler({ sub: 'system', email: 'system', name: 'system', entity: 'UBI', claims: ['role:admin'] }) + : { error: `read model ${key} not registered` }; + } + + // it, records, property, survey, mqc own their own routes now. + @Get('audit/exceptions') audit(): unknown { return this.query('audit.exceptions'); } + @Get('clinic/aggregate') clinic(): unknown { return this.query('clinic.aggregate'); } + @Get('hr/hours') hr(): unknown { return this.query('hr.hours'); } + @Get('finance/doc-flow') finance(): unknown { return this.query('finance.doc-flow'); } +} + +/** Uniform report-status feed (the `*.report-status` contract) — seed shape. */ +export function seedReportFeed(): ReportStatus[] { + return [ + { module: 'engineering', report: 'SWA wk 24', due: '2026-06-10', submittedAt: '2026-06-09', status: 'on-time' }, + { module: 'survey', report: 'May volumes', due: '2026-06-05', submittedAt: '2026-06-04', status: 'received' }, + { module: 'mqc', report: 'Certs — Billing No. 7', due: '2026-06-05', submittedAt: null, status: 'pending', detail: '2 pending · 6d' }, + { module: 'procurement', report: 'DR sync', due: null, submittedAt: '2026-06-11', status: 'on-time', detail: '14 today' }, + { module: 'operations', report: 'Approvals queue', due: null, submittedAt: null, status: 'pending', detail: '8 waiting' }, + { module: 'audit', report: 'Exceptions', due: null, submittedAt: null, status: 'late', detail: '2 escalated' }, + { module: 'clinic', report: 'June health summary', due: '2026-07-01', submittedAt: null, status: 'aggregate-only' }, + ]; +} + +@Module({ + imports: [DocTrackingModule], + controllers: [StubsController], + providers: [StubReadModelsService], +}) +export class StubsModule {} diff --git a/apps/api/src/modules/survey/survey.module.ts b/apps/api/src/modules/survey/survey.module.ts new file mode 100644 index 0000000..da633c3 --- /dev/null +++ b/apps/api/src/modules/survey/survey.module.ts @@ -0,0 +1,223 @@ +import { + Body, + Controller, + Get, + Module, + OnModuleInit, + Param, + Post, + Req, +} from '@nestjs/common'; +import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import { + Column, + Entity, + Repository, +} from 'typeorm'; +import type { TrendPoint } from '@ubi/types'; +import { BaseAppEntity } from '../../common/base.entity'; +import { Claims } from '../../common/rbac'; +import { ReadModelRegistry } from '../../pillars/ai-layer/read-model.registry'; +import { AiLayerModule } from '../../pillars/ai-layer/ai-layer.module'; + +// ──────────────────────────── Entities ──────────────────────────── + +/** + * Weekly/monthly measurement captured by QE — volume by pay item + chainage. + */ +@Entity('survey_measurement') +export class SurveyMeasurement extends BaseAppEntity { + @Column({ name: 'project_id', type: 'varchar' }) + projectId: string; + + /** STA 0+000 format — start of measured section. */ + @Column({ name: 'station_start', type: 'varchar', length: 16 }) + stationStart: string; + + /** STA 0+000 format — end of measured section. */ + @Column({ name: 'station_end', type: 'varchar', length: 16 }) + stationEnd: string; + + /** DPWH Blue Book pay-item number, e.g. "311(1)c". */ + @Column({ name: 'pay_item_no', type: 'varchar', length: 32 }) + payItemNo: string; + + /** Measured volume (m³). */ + @Column({ name: 'volume_m3', type: 'numeric', precision: 12, scale: 3 }) + volumeM3: number; + + /** Measurement type: original-ground | stakeout | cross-section | as-built */ + @Column({ type: 'varchar', length: 32, default: 'cross-section' }) + type: string; + + /** YYYY-MM reporting period. */ + @Column({ name: 'period', type: 'varchar', length: 7 }) + period: string; + + @Column({ name: 'surveyor_id', type: 'varchar', nullable: true }) + surveyorId: string | null; + + @Column({ name: 'notes', type: 'text', nullable: true }) + notes: string | null; +} + +/** + * Cross-section data point — area at a station (feeds quantity computation). + */ +@Entity('survey_cross_section') +export class SurveyCrossSection extends BaseAppEntity { + @Column({ name: 'project_id', type: 'varchar' }) + projectId: string; + + @Column({ name: 'station', type: 'varchar', length: 16 }) + station: string; + + /** Pay item this cross-section belongs to. */ + @Column({ name: 'pay_item_no', type: 'varchar', length: 32 }) + payItemNo: string; + + /** Cross-sectional area in m². */ + @Column({ name: 'area_m2', type: 'numeric', precision: 10, scale: 4 }) + areaM2: number; + + /** Interval length used for prismoidal formula (m). */ + @Column({ name: 'interval_m', type: 'numeric', precision: 8, scale: 3, nullable: true }) + intervalM: number | null; + + @Column({ name: 'computed_volume_m3', type: 'numeric', precision: 12, scale: 3, nullable: true }) + computedVolumeM3: number | null; +} + +// ──────────────────────────── Service ──────────────────────────── + +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class SurveyService implements OnModuleInit { + constructor( + @InjectRepository(SurveyMeasurement) private readonly measurements: Repository, + @InjectRepository(SurveyCrossSection) private readonly sections: Repository, + private readonly registry: ReadModelRegistry, + ) {} + + onModuleInit(): void { + this.registry.register( + { + key: 'survey.volumes', + module: 'survey', + title: 'Monthly survey volumes (m³)', + description: 'Aggregated from real measurements; station/chainage indexed. Feeds ManCom.', + }, + async () => this.monthlyTrend(), + ); + } + + async monthlyTrend(): Promise { + // Aggregate real measurements grouped by period. + const rows: { period: string; total: string }[] = await this.measurements + .createQueryBuilder('m') + .select('m.period', 'period') + .addSelect('SUM(m.volume_m3)', 'total') + .where('m.deleted_at IS NULL') + .groupBy('m.period') + .orderBy('m.period', 'ASC') + .getRawMany(); + + return rows.map((r) => ({ + period: r.period, + plan: null, + actual: Math.round(Number(r.total)), + projected: null, + })); + } + + async listByProject(projectId: string): Promise { + return this.measurements.find({ + where: { projectId }, + order: { period: 'ASC', stationStart: 'ASC' }, + withDeleted: false, + }); + } + + async submit(data: Partial, actor: string): Promise { + const m = this.measurements.create({ ...data, createdBy: actor }); + return this.measurements.save(m); + } + + async crossSections(projectId: string, payItemNo?: string): Promise { + const qb = this.sections + .createQueryBuilder('cs') + .where('cs.project_id = :pid', { pid: projectId }) + .andWhere('cs.deleted_at IS NULL'); + if (payItemNo) qb.andWhere('cs.pay_item_no = :item', { item: payItemNo }); + return qb.orderBy('cs.station', 'ASC').getMany(); + } + + async submitCrossSection(data: Partial, actor: string): Promise { + const cs = this.sections.create({ ...data, createdBy: actor }); + return this.sections.save(cs); + } +} + +// ──────────────────────────── Controller ──────────────────────────── + +interface JwtRequest { + user: { sub: string; email: string; name: string; entity: string; claims: string[] }; +} + +@Controller('survey') +export class SurveyController { + constructor(private readonly survey: SurveyService) {} + + @Get('volumes') + trend(): Promise { + return this.survey.monthlyTrend(); + } + + @Get('projects/:projectId') + @Claims('role:manager', 'dept:survey', 'dept:engineering', 'role:ceo', 'role:vpo') + listByProject(@Param('projectId') projectId: string): Promise { + return this.survey.listByProject(projectId); + } + + @Post('projects/:projectId/measurements') + @Claims('dept:survey', 'role:manager') + submitMeasurement( + @Param('projectId') projectId: string, + @Body() body: Partial, + @Req() req: JwtRequest, + ): Promise { + return this.survey.submit({ ...body, projectId }, req.user.email); + } + + @Get('projects/:projectId/cross-sections') + @Claims('role:manager', 'dept:survey', 'dept:engineering', 'role:ceo', 'role:vpo') + crossSections( + @Param('projectId') projectId: string, + ): Promise { + return this.survey.crossSections(projectId); + } + + @Post('projects/:projectId/cross-sections') + @Claims('dept:survey', 'role:manager') + submitCrossSection( + @Param('projectId') projectId: string, + @Body() body: Partial, + @Req() req: JwtRequest, + ): Promise { + return this.survey.submitCrossSection({ ...body, projectId }, req.user.email); + } +} + +// ──────────────────────────── Module ──────────────────────────── + +@Module({ + imports: [ + TypeOrmModule.forFeature([SurveyMeasurement, SurveyCrossSection]), + AiLayerModule, + ], + controllers: [SurveyController], + providers: [SurveyService], + exports: [SurveyService], +}) +export class SurveyModule {} diff --git a/apps/api/src/pillars/ai-layer/ai-layer.controller.ts b/apps/api/src/pillars/ai-layer/ai-layer.controller.ts new file mode 100644 index 0000000..1b2b08c --- /dev/null +++ b/apps/api/src/pillars/ai-layer/ai-layer.controller.ts @@ -0,0 +1,100 @@ +import { Body, Controller, ForbiddenException, Get, NotFoundException, Param, Post } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { AskAnswer, ReadModelDescriptor, Severity, UserClaims } from '@ubi/types'; +import { IsArray, IsIn, IsOptional, IsString, MinLength } from 'class-validator'; +import { Repository } from 'typeorm'; +import { CurrentUser, RequireClaims, hasClaim } from '../../common/rbac'; +import { InsightEntity } from './insight.entity'; +import { ReadModelRegistry } from './read-model.registry'; + +class AskDto { + @IsString() @MinLength(2) question: string; +} + +class PostInsightDto { + @IsIn(['critical', 'warning', 'positive', 'info']) severity: Severity; + @IsString() @MinLength(4) title: string; + @IsString() @MinLength(4) body: string; + @IsArray() evidence: string[]; + @IsOptional() @IsArray() actions?: string[]; +} + +@Controller('ai') +export class AiLayerController { + constructor( + private readonly registry: ReadModelRegistry, + @InjectRepository(InsightEntity) private readonly insights: Repository, + ) {} + + @Get('read-models') + list(): ReadModelDescriptor[] { + return this.registry.list(); + } + + @Get('read-models/:key') + async query(@CurrentUser() user: UserClaims, @Param('key') key: string): Promise { + const model = this.registry.get(key); + if (!model) throw new NotFoundException(`no read model ${key}`); + const required = model.descriptor.requiresClaim; + if (required && !hasClaim(user, required)) { + throw new ForbiddenException(`read model ${key} requires ${required}`); + } + return model.handler(user); + } + + /** + * Naive grounded answerer for the scaffold: surfaces which read-models the + * question touches. The real reasoning runs on an OFF-BOX agent that calls + * the same registry — this endpoint is its on-box fallback and contract. + */ + @Post('ask') + async ask(@CurrentUser() user: UserClaims, @Body() dto: AskDto): Promise { + const q = dto.question.toLowerCase(); + const hits = this.registry + .list() + .filter( + (m) => + !m.requiresClaim || hasClaim(user, m.requiresClaim) + ? q.includes(m.module) || m.title.toLowerCase().split(' ').some((w) => w.length > 4 && q.includes(w)) + : false, + ) + .slice(0, 4); + const sources = (hits.length ? hits : this.registry.list().slice(0, 3)).map((m) => m.key); + const latest = await this.insights.find({ + where: { dismissed: false }, + order: { createdAt: 'DESC' }, + take: 3, + }); + const digest = latest.map((i) => `• ${i.title}`).join('\n'); + return { + question: dto.question, + answer: + `Scaffold answer (off-box agent not yet wired). Current top signals:\n${digest}\n` + + `Queryable sources matched: ${sources.join(', ')}.`, + sources, + answeredAt: new Date().toISOString(), + }; + } + + /** Off-box agents post their generated insights back through this gate. */ + @RequireClaims('agent:insights', 'role:admin') + @Post('insights') + postInsight(@CurrentUser() user: UserClaims, @Body() dto: PostInsightDto): Promise { + const row = this.insights.create({ + severity: dto.severity, + title: dto.title, + body: dto.body, + evidence: dto.evidence, + actions: dto.actions ?? [], + generatedBy: user.email, + entity: user.entity, + createdBy: user.email, + }); + return this.insights.save(row); + } + + @Get('insights') + listInsights(): Promise { + return this.insights.find({ where: { dismissed: false }, order: { createdAt: 'DESC' }, take: 50 }); + } +} diff --git a/apps/api/src/pillars/ai-layer/ai-layer.module.ts b/apps/api/src/pillars/ai-layer/ai-layer.module.ts new file mode 100644 index 0000000..4b9a675 --- /dev/null +++ b/apps/api/src/pillars/ai-layer/ai-layer.module.ts @@ -0,0 +1,15 @@ +import { Global, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AiLayerController } from './ai-layer.controller'; +import { InsightEntity } from './insight.entity'; +import { ReadModelRegistry } from './read-model.registry'; + +/** Pillar 3 — global so every module can register its read surface. */ +@Global() +@Module({ + imports: [TypeOrmModule.forFeature([InsightEntity])], + controllers: [AiLayerController], + providers: [ReadModelRegistry], + exports: [ReadModelRegistry, TypeOrmModule], +}) +export class AiLayerModule {} diff --git a/apps/api/src/pillars/ai-layer/insight.entity.ts b/apps/api/src/pillars/ai-layer/insight.entity.ts new file mode 100644 index 0000000..c0b7a6c --- /dev/null +++ b/apps/api/src/pillars/ai-layer/insight.entity.ts @@ -0,0 +1,31 @@ +import type { Severity } from '@ubi/types'; +import { Column, Entity } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; + +/** + * Insights are GENERATED OFF-BOX (hard rule 1) and posted back here. + * The box stores and serves results only. + */ +@Entity('insight') +export class InsightEntity extends BaseAppEntity { + @Column({ type: 'varchar' }) + severity: Severity; + + @Column() + title: string; + + @Column({ type: 'text' }) + body: string; + + @Column({ type: 'jsonb', default: () => `'[]'` }) + evidence: string[]; + + @Column({ type: 'jsonb', default: () => `'[]'` }) + actions: string[]; + + @Column({ name: 'generated_by' }) + generatedBy: string; + + @Column({ default: false }) + dismissed: boolean; +} diff --git a/apps/api/src/pillars/ai-layer/read-model.registry.ts b/apps/api/src/pillars/ai-layer/read-model.registry.ts new file mode 100644 index 0000000..dc1aec7 --- /dev/null +++ b/apps/api/src/pillars/ai-layer/read-model.registry.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import type { ReadModelDescriptor, UserClaims } from '@ubi/types'; + +export type ReadModelHandler = (user: UserClaims) => Promise | unknown; + +/** + * Pillar 3 — every module registers a consistent, RBAC-aware read surface + * here. Off-box agents, the CEO chat, and ManCom consolidation are all + * clients of this registry; nothing queries module tables directly. + */ +@Injectable() +export class ReadModelRegistry { + private readonly models = new Map(); + + register(descriptor: ReadModelDescriptor, handler: ReadModelHandler): void { + this.models.set(descriptor.key, { descriptor, handler }); + } + + list(): ReadModelDescriptor[] { + return [...this.models.values()].map((m) => m.descriptor); + } + + get(key: string): { descriptor: ReadModelDescriptor; handler: ReadModelHandler } | undefined { + return this.models.get(key); + } +} diff --git a/apps/api/src/pillars/auth/auth.controller.ts b/apps/api/src/pillars/auth/auth.controller.ts new file mode 100644 index 0000000..356b9db --- /dev/null +++ b/apps/api/src/pillars/auth/auth.controller.ts @@ -0,0 +1,75 @@ +import { + Body, + Controller, + Get, + HttpCode, + HttpException, + HttpStatus, + NotImplementedException, + Post, + Req, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import type { UserClaims } from '@ubi/types'; +import { IsEmail, IsString, MinLength } from 'class-validator'; +import type { Request } from 'express'; +import { CurrentUser, Public } from '../../common/rbac'; +import { AuthService, LoginResult } from './auth.service'; + +/** Plain in-memory throttle for credential guessing; Redis-backed later. */ +const attempts = new Map(); +const WINDOW_MS = 60_000; +const MAX_PER_WINDOW = 10; + +function throttle(ip: string): void { + const now = Date.now(); + const slot = attempts.get(ip); + if (!slot || slot.resetAt < now) { + attempts.set(ip, { count: 1, resetAt: now + WINDOW_MS }); + return; + } + slot.count += 1; + if (slot.count > MAX_PER_WINDOW) { + throw new HttpException('too many login attempts — wait a minute', HttpStatus.TOO_MANY_REQUESTS); + } +} + +class LoginDto { + @IsEmail() + email: string; + + @IsString() + @MinLength(4) + password: string; +} + +@Controller('auth') +export class AuthController { + constructor( + private readonly auth: AuthService, + private readonly config: ConfigService, + ) {} + + @Public() + @HttpCode(200) + @Post('login') + login(@Req() req: Request, @Body() dto: LoginDto): Promise { + throttle(req.ip ?? req.socket.remoteAddress ?? 'unknown'); + return this.auth.login(dto.email, dto.password); + } + + @Get('me') + me(@CurrentUser() user: UserClaims): UserClaims { + return user; + } + + /** Sign in with Google — optional layer on standalone auth (Pillar 1). */ + @Public() + @Get('google') + google(): never { + if (!this.config.get('GOOGLE_CLIENT_ID')) { + throw new NotImplementedException('Google OAuth not configured (set GOOGLE_CLIENT_ID/SECRET)'); + } + throw new NotImplementedException('Google OAuth flow lands in pillar-auth follow-up'); + } +} diff --git a/apps/api/src/pillars/auth/auth.module.ts b/apps/api/src/pillars/auth/auth.module.ts new file mode 100644 index 0000000..bc8686b --- /dev/null +++ b/apps/api/src/pillars/auth/auth.module.ts @@ -0,0 +1,30 @@ +import { Global, Module } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { UserEntity } from './user.entity'; + +@Global() +@Module({ + imports: [ + TypeOrmModule.forFeature([UserEntity]), + JwtModule.registerAsync({ + global: true, + inject: [ConfigService], + useFactory: (config: ConfigService) => { + const secret = config.get('JWT_SECRET'); + const isDevDefault = !secret || secret === 'change-me' || secret === 'dev-only-secret'; + if (process.env.NODE_ENV === 'production' && isDevDefault) { + throw new Error('JWT_SECRET must be set to a strong value in production'); + } + return { secret: secret ?? 'dev-only-secret' }; + }, + }), + ], + controllers: [AuthController], + providers: [AuthService], + exports: [AuthService], +}) +export class AuthModule {} diff --git a/apps/api/src/pillars/auth/auth.service.ts b/apps/api/src/pillars/auth/auth.service.ts new file mode 100644 index 0000000..5e6132f --- /dev/null +++ b/apps/api/src/pillars/auth/auth.service.ts @@ -0,0 +1,84 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService, type JwtSignOptions } from '@nestjs/jwt'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { UserClaims } from '@ubi/types'; +import * as bcrypt from 'bcryptjs'; +import { Repository } from 'typeorm'; +import { UserEntity } from './user.entity'; + +export interface LoginResult { + token: string; + user: UserClaims; +} + +@Injectable() +export class AuthService { + constructor( + @InjectRepository(UserEntity) private readonly users: Repository, + private readonly jwt: JwtService, + private readonly config: ConfigService, + ) {} + + toClaims(u: UserEntity): UserClaims { + return { sub: u.id, email: u.email, name: u.name, entity: u.entity, claims: u.claims }; + } + + async login(email: string, password: string): Promise { + const user = await this.users.findOne({ where: { email: email.toLowerCase() } }); + if (!user?.passwordHash || !(await bcrypt.compare(password, user.passwordHash))) { + throw new UnauthorizedException('bad credentials'); + } + const claims = this.toClaims(user); + // Offline-cached-login policy: field roles get the long grace window. + const expiresIn = user.isField + ? this.config.get('JWT_EXPIRES_FIELD', '72h') + : this.config.get('JWT_EXPIRES', '8h'); + const token = await this.jwt.signAsync( + { ...claims }, + { expiresIn: expiresIn as JwtSignOptions['expiresIn'] }, + ); + return { token, user: claims }; + } + + async findByEmail(email: string): Promise { + return this.users.findOne({ where: { email: email.toLowerCase() } }); + } + + async ensureUser(input: { + email: string; + name: string; + password?: string; + /** Pre-computed bcrypt hash — force-applied (rotation), overrides password. */ + passwordHash?: string; + claims: string[]; + isField?: boolean; + entity?: 'UBI' | 'OMEGA'; + }): Promise { + const existing = await this.findByEmail(input.email); + if (existing) { + let changed = false; + // Keep the demo display name in sync with the seed (idempotent). + if (input.name && existing.name !== input.name) { + existing.name = input.name; + changed = true; + } + // Force-rotate the password to the managed hash when it differs. + if (input.passwordHash && existing.passwordHash !== input.passwordHash) { + existing.passwordHash = input.passwordHash; + changed = true; + } + return changed ? this.users.save(existing) : existing; + } + const user = this.users.create({ + email: input.email.toLowerCase(), + name: input.name, + passwordHash: input.passwordHash ?? (input.password ? await bcrypt.hash(input.password, 10) : null), + claims: input.claims, + isField: input.isField ?? false, + entity: input.entity ?? 'UBI', + createdBy: 'seed', + }); + return this.users.save(user); + } +} diff --git a/apps/api/src/pillars/auth/user.entity.ts b/apps/api/src/pillars/auth/user.entity.ts new file mode 100644 index 0000000..6ee8703 --- /dev/null +++ b/apps/api/src/pillars/auth/user.entity.ts @@ -0,0 +1,28 @@ +import { Column, Entity, Index } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; + +@Entity('app_user') +export class UserEntity extends BaseAppEntity { + @Index({ unique: true }) + @Column() + email: string; + + @Column() + name: string; + + /** Null when the account is Google-only. */ + @Column({ name: 'password_hash', type: 'varchar', nullable: true }) + passwordHash: string | null; + + /** Linked Google subject id, when "Sign in with Google" is attached. */ + @Column({ name: 'google_sub', type: 'varchar', nullable: true }) + googleSub: string | null; + + /** dept:engineering, role:manager, scope:project-, role:ceo … */ + @Column({ type: 'jsonb', default: () => `'[]'` }) + claims: string[]; + + /** Field roles get long offline grace; office-sensitive roles short sessions. */ + @Column({ name: 'is_field', default: false }) + isField: boolean; +} diff --git a/apps/api/src/pillars/doc-tracking/doc-tracking.controller.ts b/apps/api/src/pillars/doc-tracking/doc-tracking.controller.ts new file mode 100644 index 0000000..2c9e3de --- /dev/null +++ b/apps/api/src/pillars/doc-tracking/doc-tracking.controller.ts @@ -0,0 +1,59 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import type { UserClaims, WhereIsAnswer } from '@ubi/types'; +import { IsOptional, IsString, MinLength } from 'class-validator'; +import { CurrentUser } from '../../common/rbac'; +import { DocTrackingService } from './doc-tracking.service'; +import { DocumentEntity } from './document.entity'; + +class CreateDocDto { + @IsString() @MinLength(2) type: string; + @IsString() @MinLength(2) title: string; + @IsOptional() @IsString() location?: string; +} + +class TransmitDto { + @IsString() @MinLength(2) to: string; + @IsOptional() @IsString() note?: string; +} + +class ReceiveDto { + @IsOptional() @IsString() location?: string; +} + +@Controller('docs') +export class DocTrackingController { + constructor(private readonly svc: DocTrackingService) {} + + @Post() + create(@CurrentUser() user: UserClaims, @Body() dto: CreateDocDto): Promise { + return this.svc.create(user, dto); + } + + @Get() + list(): Promise { + return this.svc.list(); + } + + @Get('where/:code') + whereIs(@Param('code') code: string): Promise { + return this.svc.whereIs(code); + } + + @Post(':code/transmit') + transmit( + @CurrentUser() user: UserClaims, + @Param('code') code: string, + @Body() dto: TransmitDto, + ): Promise { + return this.svc.transmit(user, code, dto.to, dto.note); + } + + @Post(':code/receive') + receive( + @CurrentUser() user: UserClaims, + @Param('code') code: string, + @Body() dto: ReceiveDto, + ): Promise { + return this.svc.receive(user, code, dto.location); + } +} diff --git a/apps/api/src/pillars/doc-tracking/doc-tracking.module.ts b/apps/api/src/pillars/doc-tracking/doc-tracking.module.ts new file mode 100644 index 0000000..c331d73 --- /dev/null +++ b/apps/api/src/pillars/doc-tracking/doc-tracking.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DocTrackingController } from './doc-tracking.controller'; +import { DocTrackingService } from './doc-tracking.service'; +import { DocumentMovementEntity } from './document-movement.entity'; +import { DocumentEntity } from './document.entity'; + +/** Pillar 2 — the backbone. Every department routes paper through here. */ +@Module({ + imports: [TypeOrmModule.forFeature([DocumentEntity, DocumentMovementEntity])], + controllers: [DocTrackingController], + providers: [DocTrackingService], + exports: [DocTrackingService], +}) +export class DocTrackingModule {} diff --git a/apps/api/src/pillars/doc-tracking/doc-tracking.service.ts b/apps/api/src/pillars/doc-tracking/doc-tracking.service.ts new file mode 100644 index 0000000..a914325 --- /dev/null +++ b/apps/api/src/pillars/doc-tracking/doc-tracking.service.ts @@ -0,0 +1,133 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { UserClaims, WhereIsAnswer } from '@ubi/types'; +import { Repository } from 'typeorm'; +import { DocumentMovementEntity } from './document-movement.entity'; +import { DocumentEntity } from './document.entity'; + +@Injectable() +export class DocTrackingService { + constructor( + @InjectRepository(DocumentEntity) private readonly docs: Repository, + @InjectRepository(DocumentMovementEntity) private readonly moves: Repository, + ) {} + + private async nextCode(): Promise { + const year = new Date().getFullYear(); + const count = await this.docs.count(); + return `DOC-${year}-${String(count + 1).padStart(5, '0')}`; + } + + async create( + user: UserClaims, + input: { type: string; title: string; location?: string }, + ): Promise { + const doc = this.docs.create({ + code: await this.nextCode(), + type: input.type, + title: input.title, + status: 'with-holder', + currentHolder: user.name, + currentLocation: input.location ?? 'origin', + entity: user.entity, + createdBy: user.email, + }); + const saved = await this.docs.save(doc); + await this.moves.save( + this.moves.create({ + documentId: saved.id, + from: '—', + to: user.name, + by: user.email, + kind: 'create', + note: 'labeled + registered', + entity: user.entity, + createdBy: user.email, + }), + ); + return saved; + } + + async byCode(code: string): Promise { + const doc = await this.docs.findOne({ where: { code } }); + if (!doc) throw new NotFoundException(`no document ${code}`); + return doc; + } + + /** Scan to transmit: holder hands the paper to a destination. */ + async transmit(user: UserClaims, code: string, to: string, note?: string): Promise { + const doc = await this.byCode(code); + await this.moves.save( + this.moves.create({ + documentId: doc.id, + from: doc.currentHolder, + to, + by: user.email, + kind: 'transmit', + note: note ?? null, + entity: doc.entity, + createdBy: user.email, + }), + ); + doc.status = 'in-transit'; + doc.currentHolder = to; + return this.docs.save(doc); + } + + /** Scan to receive: destination confirms the paper arrived. */ + async receive(user: UserClaims, code: string, location?: string): Promise { + const doc = await this.byCode(code); + await this.moves.save( + this.moves.create({ + documentId: doc.id, + from: doc.currentHolder, + to: user.name, + by: user.email, + kind: 'receive', + note: location ? `received at ${location}` : null, + entity: doc.entity, + createdBy: user.email, + }), + ); + doc.status = 'received'; + doc.currentHolder = user.name; + if (location) doc.currentLocation = location; + return this.docs.save(doc); + } + + /** The backbone question: where is this document right now? */ + async whereIs(code: string): Promise { + const doc = await this.byCode(code); + const history = await this.moves.find({ + where: { documentId: doc.id }, + order: { createdAt: 'ASC' }, + }); + return { + document: { + id: doc.id, + code: doc.code, + entity: doc.entity, + type: doc.type, + title: doc.title, + status: doc.status, + currentHolder: doc.currentHolder, + currentLocation: doc.currentLocation, + createdAt: doc.createdAt.toISOString(), + }, + history: history.map((m) => ({ + id: m.id, + documentId: m.documentId, + from: m.from, + to: m.to, + by: m.by, + at: m.createdAt.toISOString(), + kind: m.kind, + note: m.note ?? undefined, + })), + }; + } + + async list(): Promise { + return this.docs.find({ order: { createdAt: 'DESC' }, take: 100 }); + } +} diff --git a/apps/api/src/pillars/doc-tracking/document-movement.entity.ts b/apps/api/src/pillars/doc-tracking/document-movement.entity.ts new file mode 100644 index 0000000..ed789fc --- /dev/null +++ b/apps/api/src/pillars/doc-tracking/document-movement.entity.ts @@ -0,0 +1,29 @@ +import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; +import { DocumentEntity } from './document.entity'; + +@Entity('document_movement') +export class DocumentMovementEntity extends BaseAppEntity { + @Index() + @Column({ name: 'document_id' }) + documentId: string; + + @ManyToOne(() => DocumentEntity, (d) => d.movements, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'document_id' }) + document: DocumentEntity; + + @Column({ name: 'from_party' }) + from: string; + + @Column({ name: 'to_party' }) + to: string; + + @Column() + by: string; + + @Column({ type: 'varchar' }) + kind: 'transmit' | 'receive' | 'create'; + + @Column({ type: 'varchar', nullable: true }) + note: string | null; +} diff --git a/apps/api/src/pillars/doc-tracking/document.entity.ts b/apps/api/src/pillars/doc-tracking/document.entity.ts new file mode 100644 index 0000000..396607d --- /dev/null +++ b/apps/api/src/pillars/doc-tracking/document.entity.ts @@ -0,0 +1,30 @@ +import type { DocumentStatus } from '@ubi/types'; +import { Column, Entity, Index, OneToMany } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; +import { DocumentMovementEntity } from './document-movement.entity'; + +@Entity('document') +export class DocumentEntity extends BaseAppEntity { + /** Human/QR code, e.g. DOC-2026-04187 — what the label printer prints. */ + @Index({ unique: true }) + @Column() + code: string; + + @Column() + type: string; + + @Column() + title: string; + + @Column({ type: 'varchar', default: 'with-holder' }) + status: DocumentStatus; + + @Column({ name: 'current_holder' }) + currentHolder: string; + + @Column({ name: 'current_location' }) + currentLocation: string; + + @OneToMany(() => DocumentMovementEntity, (m) => m.document) + movements: DocumentMovementEntity[]; +} diff --git a/apps/api/src/seed/credentials.ts b/apps/api/src/seed/credentials.ts new file mode 100644 index 0000000..dcae6dd --- /dev/null +++ b/apps/api/src/seed/credentials.ts @@ -0,0 +1,27 @@ +// Managed account password hashes (bcrypt — one-way, safe to commit). +// Plaintexts are NOT stored here. Pilot password for ALL accounts is shared +// (set/known via chat). ensureUser force-applies these on boot; rotate by +// regenerating this map. +const ALL = '$2a$10$iyM9J8mhDIzDJSc64pqH9uNlVdJCfMIyLexx0B3/dkUroX9o0RJEO'; + +export const CREDENTIAL_HASHES: Record = { + 'admin@ubi.ph': ALL, + 'ceo@ubi.ph': ALL, + 'vpo@ubi.ph': ALL, + 'pm@ubi.ph': ALL, + 'pe@ubi.ph': ALL, + 'insights-agent@ubi.ph': ALL, + 'head-engineering@ubi.ph': ALL, + 'head-procurement@ubi.ph': ALL, + 'head-operations@ubi.ph': ALL, + 'head-survey@ubi.ph': ALL, + 'head-mqc@ubi.ph': ALL, + 'head-audit@ubi.ph': ALL, + 'head-it@ubi.ph': ALL, + 'head-records@ubi.ph': ALL, + 'head-clinic@ubi.ph': ALL, + 'head-admin@ubi.ph': ALL, + 'head-hr@ubi.ph': ALL, + 'head-property@ubi.ph': ALL, + 'head-finance@ubi.ph': ALL, +}; diff --git a/apps/api/src/seed/seed.module.ts b/apps/api/src/seed/seed.module.ts new file mode 100644 index 0000000..154ef84 --- /dev/null +++ b/apps/api/src/seed/seed.module.ts @@ -0,0 +1,219 @@ +import { Injectable, Logger, Module, OnApplicationBootstrap } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { UserClaims } from '@ubi/types'; +import { Repository } from 'typeorm'; +import { InsightEntity } from '../pillars/ai-layer/insight.entity'; +import { AuthService } from '../pillars/auth/auth.service'; +import { DocTrackingModule } from '../pillars/doc-tracking/doc-tracking.module'; +import { DocTrackingService } from '../pillars/doc-tracking/doc-tracking.service'; +import { DEPARTMENTS } from '../modules/dept/dept.module'; +import { CREDENTIAL_HASHES } from './credentials'; +import { EngineeringModule } from '../modules/engineering/engineering.module'; +import { PayItemEntity, ProjectEntity } from '../modules/engineering/entities'; +import { EquipmentModule, EquipmentService } from '../modules/equipment/equipment.module'; +import { ItModule, ItService } from '../modules/it/it.module'; +import { MqcModule, MqcService } from '../modules/mqc/mqc.module'; +import { OmegaModule, OmegaService } from '../modules/omega/omega.module'; +import { PropertyModule, PropertyService } from '../modules/property/property.module'; +import { QuantityModule, QuantityService } from '../modules/quantity/quantity.module'; +import { RecordsModule, RecordsService } from '../modules/records/records.module'; +import { SurveyModule, SurveyService } from '../modules/survey/survey.module'; +import { ExpiryService } from '../shared/expiry/expiry.module'; + +/** Dev/demo seed — idempotent; runs only when the database is empty. */ +@Injectable() +export class SeedService implements OnApplicationBootstrap { + private readonly log = new Logger('Seed'); + + constructor( + private readonly config: ConfigService, + private readonly auth: AuthService, + private readonly docs: DocTrackingService, + private readonly expiry: ExpiryService, + private readonly property: PropertyService, + private readonly records: RecordsService, + private readonly it: ItService, + private readonly quantity: QuantityService, + private readonly equipment: EquipmentService, + private readonly survey: SurveyService, + private readonly mqc: MqcService, + private readonly omega: OmegaService, + @InjectRepository(PayItemEntity) private readonly payItems: Repository, + @InjectRepository(ProjectEntity) private readonly projects: Repository, + @InjectRepository(InsightEntity) private readonly insights: Repository, + ) {} + + async onApplicationBootstrap(): Promise { + // Demo data carries PUBLIC passwords — never allowed to reach production. + if (this.config.get('SEED_DEMO') !== 'true') { + this.log.log('SEED_DEMO != true — skipping demo seed'); + return; + } + + // Accounts: passwords come from managed bcrypt hashes (CREDENTIAL_HASHES); + // ensureUser force-rotates existing records to those hashes on boot. + const h = (email: string) => CREDENTIAL_HASHES[email]; + await this.auth.ensureUser({ email: 'admin@ubi.ph', name: 'Suite Admin', passwordHash: h('admin@ubi.ph'), claims: ['role:admin'] }); + const ceo = await this.auth.ensureUser({ email: 'ceo@ubi.ph', name: 'Car Go', passwordHash: h('ceo@ubi.ph'), claims: ['role:ceo'] }); + await this.auth.ensureUser({ email: 'vpo@ubi.ph', name: 'VPO Office', passwordHash: h('vpo@ubi.ph'), claims: ['role:vpo', 'dept:operations'] }); + await this.auth.ensureUser({ email: 'pm@ubi.ph', name: 'PM Uy', passwordHash: h('pm@ubi.ph'), claims: ['role:pm', 'dept:engineering'] }); + await this.auth.ensureUser({ + email: 'pe@ubi.ph', name: 'Engr. R. dela Cruz', passwordHash: h('pe@ubi.ph'), + claims: ['role:pe', 'dept:engineering', 'scope:project-PKG-02'], isField: true, + }); + await this.auth.ensureUser({ email: 'insights-agent@ubi.ph', name: 'Insights Agent (off-box)', passwordHash: h('insights-agent@ubi.ph'), claims: ['agent:insights'] }); + for (const d of DEPARTMENTS) { + await this.auth.ensureUser({ + email: d.headEmail, name: d.headName, passwordHash: h(d.headEmail), + claims: ['role:manager', `dept:${d.slug}`], + }); + } + const ceoClaims: UserClaims = this.auth.toClaims(ceo); + + // Core reference data (pay items, projects, demo insights, a sample doc) — + // only on a fresh DB. + if ((await this.payItems.count()) === 0) { + this.log.log('empty database — seeding core reference data'); + await this.payItems.save( + [ + { itemNo: '100(3)', description: 'Clearing and grubbing', unit: 'ha' }, + { itemNo: '200', description: 'Aggregate subbase course', unit: 'm³' }, + { itemNo: '300(1)', description: 'Aggregate base course', unit: 'm³' }, + { itemNo: '311(1)c', description: 'PCC Pavement (plain) 230mm', unit: 'm²' }, + { itemNo: '404(1)', description: 'Reinforcing steel, Grade 40', unit: 'kg' }, + { itemNo: 'SPL-1', description: 'Special item — project signage', unit: 'l.s.' }, + ].map((p) => this.payItems.create({ ...p, createdBy: 'seed' })), + ); + + const packages: Array<[string, string, number]> = [ + ['PKG-02', 'Iligan Coastal Diversion', -1.2], + ['PKG-05', 'Daang Maharlika Rehab K1440', -4.6], + ['PKG-11', 'Bukidnon Boundary Rd', 3.1], + ['PKG-03', 'Cagayan Valley Access Rd', 0.4], + ['PKG-07', 'Misamis Slope Protection', -0.6], + ['PKG-09', 'Tagoloan Bridge Approach', 1.9], + ]; + await this.projects.save( + packages.map(([code, name, slip], i) => + this.projects.create({ + code, name, contractNo: `26${String(i + 1).padStart(2, '0')}-DPWH-X`, + status: 'active', slippagePct: slip, createdBy: 'seed', + }), + ), + ); + + await this.insights.save( + [ + { + severity: 'critical' as const, + title: 'Pkg 5 trends to −6.2% slippage by July', + body: + 'Cement deliveries from Solid Mix Corp arrived late 3 consecutive weeks (avg 2.4 days). ' + + 'PCCP pours on Item 311 are 1,180 m² behind program; the weekly SWA gap has widened each week since wk 22.', + evidence: ['procurement.dr-log wk22–24', 'engineering.swa wk22–24', 'survey.volumes Δ STA 4+200–5+600'], + actions: ['view-evidence', 'send-to-vpo', 'raise-audit-exception'], + }, + { + severity: 'warning' as const, + title: 'Billing No. 7 (Pkg 2) blocked by MQC certs', + body: '2 material certificates (Item 311 beam tests) pending 6 days. Billing draft is otherwise complete.', + evidence: ['mqc.certs', 'engineering.billing №7'], + actions: ['view-evidence', 'nudge-mqc'], + }, + { + severity: 'warning' as const, + title: '4 vehicle registrations expire within 15 days', + body: '3 dump trucks (Pkg 5 haul fleet) + 1 service vehicle. Renewal lead time averages 9 days.', + evidence: ['records.expiry'], + actions: ['view-list', 'notify-records'], + }, + { + severity: 'positive' as const, + title: 'Pkg 11 ahead of plan 6 consecutive weeks', + body: '+3.1% and climbing; subbase complete 3 weeks early. Consider advancing Billing No. 4.', + evidence: ['engineering.swa', 'operations.health'], + actions: ['view-project'], + }, + ].map((i) => this.insights.create({ ...i, generatedBy: 'seed (off-box agent pattern)', createdBy: 'seed' })), + ); + + const doc = await this.docs.create(ceoClaims, { + type: 'transmittal', + title: 'Billing No. 7 — attachments batch (PKG-02)', + location: 'HQ · Engineering 3F', + }); + await this.docs.transmit(ceoClaims, doc.code, 'Finance — J. Ramos', 'for cashier batching'); + } + + // Module data — each seed is INDEPENDENTLY idempotent, so always run them. + // This backfills newly-deployed modules (Quantity in-house amounts, Property + // assets, IT/Records/Equipment) onto an existing demo DB — no DB drop needed. + await this.property.seed('seed'); + await this.records.seed('seed'); + await this.it.seed(ceoClaims); + await this.quantity.seed(); + const pkg05 = await this.projects.findOne({ where: { code: 'PKG-05' } }); + if (pkg05) await this.equipment.seed(ceoClaims, pkg05.id); + + await this.seedSurvey(); + await this.seedMqc(); + await this.omega.seed('seed'); + + this.log.log('seed complete — ceo@ubi.ph/ceo123, admin/vpo/pm/pe, + dept heads head-@ubi.ph/head123'); + } + + private async seedSurvey(): Promise { + // Seed monthly survey measurements for PKG-02 and PKG-05 if none exist. + const existing = await this.survey.listByProject('PKG-02'); + if (existing.length > 0) return; + + const records = [ + { projectId: 'PKG-02', period: '2026-01', stationStart: 'STA 0+000', stationEnd: 'STA 0+480', payItemNo: '311(1)c', volumeM3: 44200, type: 'cross-section' }, + { projectId: 'PKG-02', period: '2026-02', stationStart: 'STA 0+480', stationEnd: 'STA 1+020', payItemNo: '311(1)c', volumeM3: 54100, type: 'cross-section' }, + { projectId: 'PKG-02', period: '2026-03', stationStart: 'STA 1+020', stationEnd: 'STA 1+620', payItemNo: '311(1)c', volumeM3: 59300, type: 'cross-section' }, + { projectId: 'PKG-02', period: '2026-04', stationStart: 'STA 1+620', stationEnd: 'STA 2+080', payItemNo: '311(1)c', volumeM3: 50000, type: 'cross-section' }, + { projectId: 'PKG-02', period: '2026-05', stationStart: 'STA 2+080', stationEnd: 'STA 2+720', payItemNo: '311(1)c', volumeM3: 64400, type: 'cross-section' }, + { projectId: 'PKG-02', period: '2026-06', stationStart: 'STA 2+720', stationEnd: 'STA 3+060', payItemNo: '311(1)c', volumeM3: 31000, type: 'cross-section' }, + { projectId: 'PKG-05', period: '2026-05', stationStart: 'STA 4+200', stationEnd: 'STA 5+600', payItemNo: '200', volumeM3: 38800, type: 'as-built' }, + { projectId: 'PKG-05', period: '2026-06', stationStart: 'STA 5+600', stationEnd: 'STA 6+200', payItemNo: '200', volumeM3: 21200, type: 'as-built' }, + ]; + + for (const r of records) { + await this.survey.submit(r, 'seed'); + } + this.log.log(`survey: seeded ${records.length} measurements`); + } + + private async seedMqc(): Promise { + const existing = await this.mqc.listTests('PKG-02'); + if (existing.length > 0) return; + + const tests = [ + { projectId: 'PKG-02', payItemNo: '311(1)c', testType: 'Concrete beam (7-day)', result: 'pending', billingRef: 'Billing No. 7 / PKG-02', certId: 'MQC-2026-0712', testDate: new Date('2026-06-08') }, + { projectId: 'PKG-02', payItemNo: '311(1)c', testType: 'Concrete beam (14-day)', result: 'pending', billingRef: 'Billing No. 7 / PKG-02', certId: 'MQC-2026-0713', testDate: new Date('2026-06-08') }, + { projectId: 'PKG-02', payItemNo: '311(1)c', testType: 'Concrete beam (28-day)', result: 'passed', billingRef: 'Billing No. 6 / PKG-02', certId: 'MQC-2026-0688', testDate: new Date('2026-05-20'), labRef: 'LAB-0512' }, + { projectId: 'PKG-05', payItemNo: '200', testType: 'Field density (nuclear)', result: 'passed', testDate: new Date('2026-05-28'), labRef: 'LAB-0498' }, + { projectId: 'PKG-05', payItemNo: '200', testType: 'CBR laboratory', result: 'passed', testDate: new Date('2026-04-10'), labRef: 'LAB-0431' }, + ]; + + for (const t of tests) { + await this.mqc.submitTest(t, 'seed'); + } + + const pours = [ + { projectId: 'PKG-02', payItemNo: '311(1)c', station: 'STA 2+720', pourDate: new Date('2026-06-08'), mixDesign: 'Class A 40.7 MPa', volumeM3: 180.5, slumpMm: 75, airContentPct: 5.2 }, + { projectId: 'PKG-02', payItemNo: '311(1)c', station: 'STA 2+840', pourDate: new Date('2026-06-10'), mixDesign: 'Class A 40.7 MPa', volumeM3: 165.0, slumpMm: 80, airContentPct: 4.9 }, + ]; + for (const p of pours) { + await this.mqc.submitPour(p, 'seed'); + } + this.log.log(`mqc: seeded ${tests.length} tests, ${pours.length} pour logs`); + } +} + +@Module({ + imports: [DocTrackingModule, EngineeringModule, PropertyModule, RecordsModule, ItModule, QuantityModule, EquipmentModule, SurveyModule, MqcModule, OmegaModule], + providers: [SeedService], +}) +export class SeedModule {} diff --git a/apps/api/src/shared/approvals/approval.entity.ts b/apps/api/src/shared/approvals/approval.entity.ts new file mode 100644 index 0000000..e78b46e --- /dev/null +++ b/apps/api/src/shared/approvals/approval.entity.ts @@ -0,0 +1,41 @@ +import { Column, Entity, Index } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; + +export interface ApprovalStepState { + /** e.g. 'pe' | 'pm' | 'vpo' — labels are flow-specific. */ + step: string; + /** Claim that may act on this step, e.g. 'role:pm'. */ + actorClaim: string; + status: 'pending' | 'approved' | 'returned'; + by: string | null; + at: string | null; + note: string | null; +} + +/** + * Shared approvals engine — materials schedules, subcontracts, PRs all use + * the same chain shape (build once, reuse everywhere). + */ +@Entity('approval') +export class ApprovalEntity extends BaseAppEntity { + /** e.g. 'materials-schedule' */ + @Index() + @Column({ name: 'subject_type' }) + subjectType: string; + + @Index() + @Column({ name: 'subject_id' }) + subjectId: string; + + @Column() + title: string; + + @Column({ type: 'jsonb' }) + chain: ApprovalStepState[]; + + @Column({ name: 'current_step', type: 'int', default: 0 }) + currentStep: number; + + @Column({ type: 'varchar', default: 'pending' }) + status: 'pending' | 'approved' | 'returned'; +} diff --git a/apps/api/src/shared/approvals/approvals.controller.ts b/apps/api/src/shared/approvals/approvals.controller.ts new file mode 100644 index 0000000..1411598 --- /dev/null +++ b/apps/api/src/shared/approvals/approvals.controller.ts @@ -0,0 +1,30 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import type { UserClaims } from '@ubi/types'; +import { IsIn, IsOptional, IsString } from 'class-validator'; +import { CurrentUser } from '../../common/rbac'; +import { ApprovalEntity } from './approval.entity'; +import { ApprovalsService } from './approvals.service'; + +class ActDto { + @IsIn(['approve', 'return']) action: 'approve' | 'return'; + @IsOptional() @IsString() note?: string; +} + +@Controller('approvals') +export class ApprovalsController { + constructor(private readonly svc: ApprovalsService) {} + + @Get() + inbox(@CurrentUser() user: UserClaims): Promise { + return this.svc.inbox(user); + } + + @Post(':id/act') + act( + @CurrentUser() user: UserClaims, + @Param('id') id: string, + @Body() dto: ActDto, + ): Promise { + return this.svc.act(user, id, dto.action, dto.note); + } +} diff --git a/apps/api/src/shared/approvals/approvals.module.ts b/apps/api/src/shared/approvals/approvals.module.ts new file mode 100644 index 0000000..3f782f1 --- /dev/null +++ b/apps/api/src/shared/approvals/approvals.module.ts @@ -0,0 +1,14 @@ +import { Global, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApprovalEntity } from './approval.entity'; +import { ApprovalsController } from './approvals.controller'; +import { ApprovalsService } from './approvals.service'; + +@Global() +@Module({ + imports: [TypeOrmModule.forFeature([ApprovalEntity])], + controllers: [ApprovalsController], + providers: [ApprovalsService], + exports: [ApprovalsService], +}) +export class ApprovalsModule {} diff --git a/apps/api/src/shared/approvals/approvals.service.ts b/apps/api/src/shared/approvals/approvals.service.ts new file mode 100644 index 0000000..60157e9 --- /dev/null +++ b/apps/api/src/shared/approvals/approvals.service.ts @@ -0,0 +1,83 @@ +import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { UserClaims } from '@ubi/types'; +import { Repository } from 'typeorm'; +import { hasClaim } from '../../common/rbac'; +import { ApprovalEntity, ApprovalStepState } from './approval.entity'; + +@Injectable() +export class ApprovalsService { + constructor( + @InjectRepository(ApprovalEntity) private readonly approvals: Repository, + ) {} + + async open( + user: UserClaims, + input: { subjectType: string; subjectId: string; title: string; steps: Array> }, + ): Promise { + if (!input.steps.length) throw new BadRequestException('approval chain needs steps'); + const chain: ApprovalStepState[] = input.steps.map((s) => ({ + ...s, + status: 'pending', + by: null, + at: null, + note: null, + })); + return this.approvals.save( + this.approvals.create({ + subjectType: input.subjectType, + subjectId: input.subjectId, + title: input.title, + chain, + currentStep: 0, + status: 'pending', + entity: user.entity, + createdBy: user.email, + }), + ); + } + + /** Inbox: chains whose CURRENT step this user may act on. */ + async inbox(user: UserClaims): Promise { + const pending = await this.approvals.find({ where: { status: 'pending' }, order: { createdAt: 'ASC' } }); + return pending.filter((a) => hasClaim(user, a.chain[a.currentStep].actorClaim)); + } + + async act( + user: UserClaims, + id: string, + action: 'approve' | 'return', + note?: string, + ): Promise { + const approval = await this.approvals.findOne({ where: { id } }); + if (!approval) throw new NotFoundException('no such approval'); + if (approval.status !== 'pending') throw new BadRequestException(`already ${approval.status}`); + const step = approval.chain[approval.currentStep]; + if (!hasClaim(user, step.actorClaim)) { + throw new ForbiddenException(`step ${step.step} requires ${step.actorClaim}`); + } + step.by = user.email; + step.at = new Date().toISOString(); + step.note = note ?? null; + if (action === 'return') { + step.status = 'returned'; + approval.status = 'returned'; + } else { + step.status = 'approved'; + if (approval.currentStep + 1 < approval.chain.length) { + approval.currentStep += 1; + } else { + approval.status = 'approved'; + } + } + return this.approvals.save(approval); + } + + async forSubject(subjectType: string, subjectId: string): Promise { + return this.approvals.findOne({ where: { subjectType, subjectId }, order: { createdAt: 'DESC' } }); + } + + async countPending(): Promise { + return this.approvals.count({ where: { status: 'pending' } }); + } +} diff --git a/apps/api/src/shared/expiry/expiry.module.ts b/apps/api/src/shared/expiry/expiry.module.ts new file mode 100644 index 0000000..a840764 --- /dev/null +++ b/apps/api/src/shared/expiry/expiry.module.ts @@ -0,0 +1,86 @@ +import { Controller, Get, Global, Injectable, Module, Query } from '@nestjs/common'; +import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import { Column, Entity, Repository } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; +import { NotificationsModule, NotificationsService } from '../notifications/notifications.module'; + +/** + * Shared expiry/renewal reminders — vehicle registration, insurance, + * licenses/warranties (IT), property docs. Records is the heaviest user. + */ +@Entity('expiry_item') +export class ExpiryItemEntity extends BaseAppEntity { + @Column() + kind: string; + + @Column({ name: 'ref_module' }) + refModule: string; + + @Column() + title: string; + + @Column({ name: 'expires_on', type: 'date' }) + expiresOn: string; + + @Column({ name: 'notify_days_before', type: 'int', default: 15 }) + notifyDaysBefore: number; + + @Column({ name: 'notify_audience', default: 'dept:records' }) + notifyAudience: string; +} + +@Injectable() +export class ExpiryService { + constructor( + @InjectRepository(ExpiryItemEntity) private readonly rows: Repository, + private readonly notifications: NotificationsService, + ) {} + + async upcoming(days: number): Promise { + const all = await this.rows.find({ order: { expiresOn: 'ASC' } }); + const cutoff = Date.now() + days * 86_400_000; + return all.filter((r) => new Date(r.expiresOn).getTime() <= cutoff); + } + + add(input: Partial & Pick): Promise { + return this.rows.save(this.rows.create({ createdBy: 'system', ...input })); + } + + /** Sweep due reminders into notifications (wire to a cron/queue later). */ + async sweep(): Promise { + const due = await this.upcoming(0 + 30); + let sent = 0; + for (const item of due) { + const daysLeft = Math.ceil((new Date(item.expiresOn).getTime() - Date.now()) / 86_400_000); + if (daysLeft <= item.notifyDaysBefore) { + await this.notifications.notify({ + toAudience: item.notifyAudience, + title: `${item.kind} expiring: ${item.title}`, + body: `Expires ${item.expiresOn} (${daysLeft} day(s) left) — ${item.refModule}`, + kind: 'expiry', + }); + sent += 1; + } + } + return sent; + } +} + +@Controller('expiry') +export class ExpiryController { + constructor(private readonly svc: ExpiryService) {} + + @Get('upcoming') + upcoming(@Query('days') days?: string): Promise { + return this.svc.upcoming(Number(days ?? 30)); + } +} + +@Global() +@Module({ + imports: [TypeOrmModule.forFeature([ExpiryItemEntity]), NotificationsModule], + controllers: [ExpiryController], + providers: [ExpiryService], + exports: [ExpiryService], +}) +export class ExpiryModule {} diff --git a/apps/api/src/shared/files/file.entity.ts b/apps/api/src/shared/files/file.entity.ts new file mode 100644 index 0000000..c660680 --- /dev/null +++ b/apps/api/src/shared/files/file.entity.ts @@ -0,0 +1,30 @@ +import { Column, Entity, Index } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; + +/** Postgres stores metadata + storage id ONLY — never binaries (hard rule 3). */ +@Entity('file') +export class FileEntity extends BaseAppEntity { + @Index({ unique: true }) + @Column({ name: 'storage_id' }) + storageId: string; + + @Column() + filename: string; + + @Column({ name: 'content_type' }) + contentType: string; + + @Column({ type: 'int' }) + size: number; + + @Column() + sha256: string; + + /** Owning context, e.g. "procurement:dr" or "engineering:progress-photo". */ + @Column() + context: string; + + /** Claim required to fetch; RBAC is enforced at fetch time (hard rule 4). */ + @Column({ name: 'requires_claim', type: 'varchar', nullable: true }) + requiresClaim: string | null; +} diff --git a/apps/api/src/shared/files/files.controller.ts b/apps/api/src/shared/files/files.controller.ts new file mode 100644 index 0000000..c20e8a4 --- /dev/null +++ b/apps/api/src/shared/files/files.controller.ts @@ -0,0 +1,63 @@ +import { + BadRequestException, + Body, + Controller, + Get, + Param, + Post, + Res, + UploadedFile, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import type { UserClaims } from '@ubi/types'; +import { IsOptional, IsString, MinLength } from 'class-validator'; +import type { Response } from 'express'; +import { CurrentUser } from '../../common/rbac'; +import { FileEntity } from './file.entity'; +import { FilesService } from './files.service'; + +class UploadDto { + @IsString() @MinLength(2) context: string; + @IsOptional() @IsString() requiresClaim?: string; +} + +@Controller('files') +export class FilesController { + constructor(private readonly svc: FilesService) {} + + /** + * Edge clients compress BEFORE upload (hard rule 5): field photos <500 KB, + * DeskGuard screenshots <200 KB. The API enforces a hard ceiling only. + */ + @Post() + @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 2 * 1024 * 1024 } })) + async upload( + @CurrentUser() user: UserClaims, + @UploadedFile() file: Express.Multer.File | undefined, + @Body() dto: UploadDto, + ): Promise { + if (!file) throw new BadRequestException('multipart field "file" is required'); + return this.svc.store(user, { + buffer: file.buffer, + filename: file.originalname, + contentType: file.mimetype, + context: dto.context, + requiresClaim: dto.requiresClaim, + }); + } + + /** RBAC-checked at fetch time; no raw paths, no public links (hard rule 4). */ + @Get(':id') + async download( + @CurrentUser() user: UserClaims, + @Param('id') id: string, + @Res() res: Response, + ): Promise { + const { meta, stream } = await this.svc.open(user, id); + res.setHeader('Content-Type', meta.contentType); + res.setHeader('Content-Length', String(meta.size)); + res.setHeader('Content-Disposition', `inline; filename="${meta.filename}"`); + stream.pipe(res); + } +} diff --git a/apps/api/src/shared/files/files.module.ts b/apps/api/src/shared/files/files.module.ts new file mode 100644 index 0000000..1b29baa --- /dev/null +++ b/apps/api/src/shared/files/files.module.ts @@ -0,0 +1,30 @@ +import { Global, Module } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { createStorageProvider, type StorageProvider } from '@ubi/storage'; +import { FileEntity } from './file.entity'; +import { FilesController } from './files.controller'; +import { FilesService } from './files.service'; +import { STORAGE } from './storage.token'; + +export { STORAGE } from './storage.token'; + +@Global() +@Module({ + imports: [TypeOrmModule.forFeature([FileEntity])], + controllers: [FilesController], + providers: [ + FilesService, + { + provide: STORAGE, + inject: [ConfigService], + useFactory: (config: ConfigService): StorageProvider => + createStorageProvider({ + driver: config.get('STORAGE_DRIVER', 'local'), + root: config.get('STORAGE_ROOT', './uploads'), + }), + }, + ], + exports: [FilesService, STORAGE], +}) +export class FilesModule {} diff --git a/apps/api/src/shared/files/files.service.ts b/apps/api/src/shared/files/files.service.ts new file mode 100644 index 0000000..6702611 --- /dev/null +++ b/apps/api/src/shared/files/files.service.ts @@ -0,0 +1,47 @@ +import { ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { StorageProvider } from '@ubi/storage'; +import type { UserClaims } from '@ubi/types'; +import type { Readable } from 'node:stream'; +import { Repository } from 'typeorm'; +import { hasClaim } from '../../common/rbac'; +import { FileEntity } from './file.entity'; +import { STORAGE } from './storage.token'; + +@Injectable() +export class FilesService { + constructor( + @InjectRepository(FileEntity) private readonly files: Repository, + @Inject(STORAGE) private readonly storage: StorageProvider, + ) {} + + async store( + user: UserClaims, + input: { buffer: Buffer; filename: string; contentType: string; context: string; requiresClaim?: string }, + ): Promise { + const stored = await this.storage.put(input.buffer, { contentType: input.contentType }); + const row = this.files.create({ + storageId: stored.id, + filename: input.filename, + contentType: input.contentType, + size: stored.size, + sha256: stored.sha256, + context: input.context, + requiresClaim: input.requiresClaim ?? null, + entity: user.entity, + createdBy: user.email, + }); + return this.files.save(row); + } + + /** Files are served through here with RBAC — never via raw paths (rule 4). */ + async open(user: UserClaims, id: string): Promise<{ meta: FileEntity; stream: Readable }> { + const meta = await this.files.findOne({ where: { id } }); + if (!meta) throw new NotFoundException('no such file'); + if (meta.requiresClaim && !hasClaim(user, meta.requiresClaim)) { + throw new ForbiddenException(`file requires ${meta.requiresClaim}`); + } + const { stream } = await this.storage.getStream(meta.storageId); + return { meta, stream }; + } +} diff --git a/apps/api/src/shared/files/storage.token.ts b/apps/api/src/shared/files/storage.token.ts new file mode 100644 index 0000000..ef5e3fe --- /dev/null +++ b/apps/api/src/shared/files/storage.token.ts @@ -0,0 +1,2 @@ +/** DI token for the StorageProvider — own file to avoid circular imports. */ +export const STORAGE = 'STORAGE_PROVIDER'; diff --git a/apps/api/src/shared/notifications/notifications.module.ts b/apps/api/src/shared/notifications/notifications.module.ts new file mode 100644 index 0000000..b92046f --- /dev/null +++ b/apps/api/src/shared/notifications/notifications.module.ts @@ -0,0 +1,72 @@ +import { Controller, Get, Global, Injectable, Module, Param, Post } from '@nestjs/common'; +import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import type { UserClaims } from '@ubi/types'; +import { Column, Entity, Index, Repository } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; +import { CurrentUser, hasClaim } from '../../common/rbac'; + +/** Shared notification service — approvals, expiries, alerts, ManCom flags. */ +@Entity('notification') +export class NotificationEntity extends BaseAppEntity { + /** Either a direct email or a claim audience like 'role:vpo'. */ + @Index() + @Column({ name: 'to_audience' }) + toAudience: string; + + @Column() + title: string; + + @Column({ type: 'text' }) + body: string; + + @Column() + kind: string; + + @Column({ default: false }) + read: boolean; +} + +@Injectable() +export class NotificationsService { + constructor( + @InjectRepository(NotificationEntity) private readonly rows: Repository, + ) {} + + notify(input: { toAudience: string; title: string; body: string; kind: string }): Promise { + return this.rows.save(this.rows.create({ ...input, createdBy: 'system' })); + } + + async mine(user: UserClaims): Promise { + const all = await this.rows.find({ where: { read: false }, order: { createdAt: 'DESC' }, take: 100 }); + return all.filter((n) => n.toAudience === user.email || hasClaim(user, n.toAudience)); + } + + async markRead(id: string): Promise { + await this.rows.update(id, { read: true }); + } +} + +@Controller('notifications') +export class NotificationsController { + constructor(private readonly svc: NotificationsService) {} + + @Get() + mine(@CurrentUser() user: UserClaims): Promise { + return this.svc.mine(user); + } + + @Post(':id/read') + async read(@Param('id') id: string): Promise<{ ok: true }> { + await this.svc.markRead(id); + return { ok: true }; + } +} + +@Global() +@Module({ + imports: [TypeOrmModule.forFeature([NotificationEntity])], + controllers: [NotificationsController], + providers: [NotificationsService], + exports: [NotificationsService], +}) +export class NotificationsModule {} diff --git a/apps/api/src/shared/ticketing/ticketing.module.ts b/apps/api/src/shared/ticketing/ticketing.module.ts new file mode 100644 index 0000000..e34d3a7 --- /dev/null +++ b/apps/api/src/shared/ticketing/ticketing.module.ts @@ -0,0 +1,196 @@ +import { + Body, + Controller, + ForbiddenException, + Get, + Global, + HttpException, + HttpStatus, + Injectable, + Logger, + Module, + Param, + Post, + Query, + Req, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import type { UserClaims } from '@ubi/types'; +import { IsEmail, IsIn, IsOptional, IsString, MinLength } from 'class-validator'; +import type { Request } from 'express'; +import { Column, Entity, Repository } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; +import { CurrentUser, Public } from '../../common/rbac'; + +/** One ticketing engine shared by IT helpdesk AND Admin inquiries. */ +@Entity('ticket') +export class TicketEntity extends BaseAppEntity { + @Column({ type: 'varchar' }) + source: 'it' | 'admin'; + + @Column() + category: string; + + @Column() + title: string; + + @Column({ type: 'text' }) + body: string; + + @Column({ type: 'varchar', default: 'open' }) + status: 'open' | 'in-progress' | 'done'; + + /** Auto-assign pairs tech location with ticket location (IT spec). */ + @Column() + location: string; + + @Column({ type: 'varchar', nullable: true }) + assignee: string | null; +} + +class CreateTicketDto { + @IsIn(['it', 'admin']) source: 'it' | 'admin'; + @IsString() @MinLength(2) category: string; + @IsString() @MinLength(4) title: string; + @IsString() @MinLength(4) body: string; + @IsString() @MinLength(2) location: string; +} + +class MoveTicketDto { + @IsIn(['open', 'in-progress', 'done']) status: 'open' | 'in-progress' | 'done'; + @IsOptional() @IsString() assignee?: string; +} + +@Injectable() +export class TicketingService { + constructor(@InjectRepository(TicketEntity) private readonly tickets: Repository) {} + + create(user: UserClaims, dto: CreateTicketDto): Promise { + return this.tickets.save( + this.tickets.create({ ...dto, entity: user.entity, createdBy: user.email }), + ); + } + + list(source?: 'it' | 'admin'): Promise { + return this.tickets.find({ + where: source ? { source } : {}, + order: { createdAt: 'DESC' }, + take: 200, + }); + } + + async move(id: string, dto: MoveTicketDto): Promise { + await this.tickets.update(id, { status: dto.status, ...(dto.assignee ? { assignee: dto.assignee } : {}) }); + return this.tickets.findOne({ where: { id } }); + } +} + +@Controller('tickets') +export class TicketingController { + constructor(private readonly svc: TicketingService) {} + + @Post() + create(@CurrentUser() user: UserClaims, @Body() dto: CreateTicketDto): Promise { + return this.svc.create(user, dto); + } + + @Get() + list(@Query('source') source?: 'it' | 'admin'): Promise { + return this.svc.list(source); + } + + @Post(':id/move') + move(@Param('id') id: string, @Body() dto: MoveTicketDto): Promise { + return this.svc.move(id, dto); + } +} + +/* ── Public helpdesk intake — no sign-in, Cloudflare Turnstile instead ── */ + +class PublicTicketDto { + @IsString() @MinLength(2) name: string; + @IsOptional() @IsEmail() email?: string; + @IsString() @MinLength(2) category: string; + @IsString() @MinLength(4) title: string; + @IsString() @MinLength(4) body: string; + @IsString() @MinLength(2) location: string; + @IsOptional() @IsString() turnstileToken?: string; +} + +/** Spam brake on top of the captcha: 5 public tickets/min/IP. */ +const publicHits = new Map(); +function publicThrottle(ip: string): void { + const now = Date.now(); + const slot = publicHits.get(ip); + if (!slot || slot.resetAt < now) { + publicHits.set(ip, { count: 1, resetAt: now + 60_000 }); + return; + } + if (++slot.count > 5) { + throw new HttpException('too many tickets — wait a minute', HttpStatus.TOO_MANY_REQUESTS); + } +} + +async function verifyTurnstile(secret: string, token: string | undefined, ip: string): Promise { + if (!token) return false; + const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ secret, response: token, remoteip: ip }), + }); + const data = (await res.json()) as { success: boolean }; + return data.success; +} + +@Controller('public/helpdesk') +export class PublicHelpdeskController { + private readonly log = new Logger('PublicHelpdesk'); + + constructor( + private readonly svc: TicketingService, + private readonly config: ConfigService, + ) {} + + @Public() + @Post() + async create(@Req() req: Request, @Body() dto: PublicTicketDto): Promise<{ id: string; status: string }> { + const ip = req.ip ?? req.socket.remoteAddress ?? 'unknown'; + publicThrottle(ip); + + const secret = this.config.get('TURNSTILE_SECRET_KEY'); + if (secret) { + if (!(await verifyTurnstile(secret, dto.turnstileToken, ip))) { + throw new ForbiddenException('captcha verification failed'); + } + } else { + // Enforced as soon as TURNSTILE_SECRET_KEY is configured (see .env.example). + this.log.warn('TURNSTILE_SECRET_KEY not set — accepting public ticket without captcha (dev mode)'); + } + + const reporter: UserClaims = { + sub: 'public', + email: dto.email ?? 'anonymous', + name: dto.name, + entity: 'UBI', + claims: [], + }; + const ticket = await this.svc.create(reporter, { + source: 'it', + category: dto.category, + title: dto.title, + body: `[reporter: ${dto.name} · ${dto.email ?? 'no email'} · ${dto.location}]\n\n${dto.body}`, + location: dto.location, + }); + return { id: ticket.id, status: ticket.status }; + } +} + +@Global() +@Module({ + imports: [TypeOrmModule.forFeature([TicketEntity])], + controllers: [TicketingController, PublicHelpdeskController], + providers: [TicketingService], + exports: [TicketingService], +}) +export class TicketingModule {} diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..ed56c25 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node", + "outDir": "dist", + "rootDir": "src", + "baseUrl": ".", + "strictPropertyInitialization": false, + "types": ["node", "multer"] + }, + "include": ["src"] +} diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..87190f6 --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,14 @@ + + + + + + + + UBI Suite + + +
+ + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..ec5adfa --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,31 @@ +{ + "name": "@ubi/web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -p tsconfig.json --noEmit && vite build", + "typecheck": "tsc -p tsconfig.json --noEmit", + "preview": "vite preview" + }, + "dependencies": { + "@fontsource-variable/space-grotesk": "^5.2.10", + "@fontsource/ibm-plex-mono": "^5.2.7", + "@types/leaflet": "^1.9.21", + "@ubi/types": "*", + "leaflet": "^1.9.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-leaflet": "^4.2.1", + "react-router-dom": "^6.28.0" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.0", + "vite": "^6.0.0", + "vite-plugin-pwa": "^0.21.1" + } +} diff --git a/apps/web/public/icon-192.png b/apps/web/public/icon-192.png new file mode 100644 index 0000000..986a4a2 Binary files /dev/null and b/apps/web/public/icon-192.png differ diff --git a/apps/web/public/icon-512.png b/apps/web/public/icon-512.png new file mode 100644 index 0000000..c27099d Binary files /dev/null and b/apps/web/public/icon-512.png differ diff --git a/apps/web/public/robots.txt b/apps/web/public/robots.txt new file mode 100644 index 0000000..25bd6ca --- /dev/null +++ b/apps/web/public/robots.txt @@ -0,0 +1,3 @@ +# POC / demo — keep the whole site out of search engines. +User-agent: * +Disallow: / diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx new file mode 100644 index 0000000..036ee39 --- /dev/null +++ b/apps/web/src/App.tsx @@ -0,0 +1,61 @@ +import { Navigate, Route, Routes } from 'react-router-dom'; +import { getToken, getUser, homeFor } from './api'; +import Shell from './Shell'; +import Approvals from './pages/Approvals'; +import Ask from './pages/Ask'; +import Dept from './pages/Dept'; +import Docs from './pages/Docs'; +import Engineering from './pages/Engineering'; +import Equipment from './pages/Equipment'; +import Fleet from './pages/Fleet'; +import Helpdesk from './pages/Helpdesk'; +import Insights from './pages/Insights'; +import It from './pages/It'; +import Login from './pages/Login'; +import ManCom from './pages/ManCom'; +import Mqc from './pages/Mqc'; +import PmsScheduler from './pages/PmsScheduler'; +import Procurement from './pages/Procurement'; +import Survey from './pages/Survey'; +import Property from './pages/Property'; +import QuantityEntry from './pages/QuantityEntry'; +import QuantityHead from './pages/QuantityHead'; +import QuantityProjection from './pages/QuantityProjection'; +import Records from './pages/Records'; + +function RequireAuth({ children }: { children: JSX.Element }) { + return getToken() ? children : ; +} + +export default function App() { + const home = homeFor(getUser()?.claims ?? []); + return ( + + } /> + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + + ); +} diff --git a/apps/web/src/AssistantFab.tsx b/apps/web/src/AssistantFab.tsx new file mode 100644 index 0000000..13a64ee --- /dev/null +++ b/apps/web/src/AssistantFab.tsx @@ -0,0 +1,83 @@ +import { FormEvent, useEffect, useRef, useState } from 'react'; +import { Link } from 'react-router-dom'; +import type { AskAnswer } from '@ubi/types'; +import { api } from './api'; +import { Type } from './ui'; + +interface Turn { who: 'me' | 'ai'; text: string; sources?: string[]; typed: boolean } + +/** Persistent icon-only AI assistant, docked bottom-right on every page. */ +export default function AssistantFab() { + const [open, setOpen] = useState(false); + const [turns, setTurns] = useState([]); + const [q, setQ] = useState(''); + const [busy, setBusy] = useState(false); + const endRef = useRef(null); + + useEffect(() => { if (open) endRef.current?.scrollIntoView({ block: 'end' }); }, [turns, busy, open]); + + async function send(question: string) { + if (!question.trim() || busy) return; + setTurns((t) => [...t, { who: 'me', text: question, typed: true }]); + setQ(''); + setBusy(true); + try { + const res = await api('/ai/ask', { method: 'POST', body: JSON.stringify({ question }) }); + setTurns((t) => [...t, { who: 'ai', text: res.answer, sources: res.sources, typed: false }]); + } catch (err) { + setTurns((t) => [...t, { who: 'ai', text: `error: ${err instanceof Error ? err.message : 'failed'}`, typed: false }]); + } finally { setBusy(false); } + } + function submit(e: FormEvent) { e.preventDefault(); void send(q); } + const markTyped = (i: number) => setTurns((t) => t.map((x, j) => (j === i ? { ...x, typed: true } : x))); + + return ( + <> + {open && ( +
+
+ Ask AI + setOpen(false)}>⤢ + +
+
+ {turns.length === 0 && ( +
+ Ask across the suite — answers cite their read-models. +
+ {["What's happening?", 'Approvals older than 3 days', 'Which billings are at risk?'].map((s) => ( + + ))} +
+
+ )} + {turns.map((t, i) => ( +
+ {t.who === 'ai' && !t.typed ? markTyped(i)} /> : t.text} + {t.typed && t.sources && ( +
{t.sources.slice(0, 4).map((s) => {s})}
+ )} +
+ ))} + {busy &&
Consulting read-models...
} +
+
+
+ setQ(e.target.value)} placeholder="Ask anything…" disabled={busy} autoFocus /> + +
+
+ )} + + + ); +} diff --git a/apps/web/src/Shell.tsx b/apps/web/src/Shell.tsx new file mode 100644 index 0000000..f9002a1 --- /dev/null +++ b/apps/web/src/Shell.tsx @@ -0,0 +1,96 @@ +import { NavLink, Outlet, useNavigate } from 'react-router-dom'; +import { clearSession, getUser, hasClaim } from './api'; +import AssistantFab from './AssistantFab'; + +const departments = [ + ['engineering', 'Engineering'], + ['procurement', 'Procurement'], + ['operations', 'Operations'], + ['survey', 'Survey'], + ['mqc', 'MQC'], + ['audit', 'Audit'], + ['it', 'IT'], + ['records', 'Records'], + ['clinic', 'Clinic'], + ['admin', 'Admin / OHS'], + ['hr', 'HR'], + ['property', 'Property'], + ['finance', 'Finance'], +] as const; + +export default function Shell() { + const user = getUser(); + const navigate = useNavigate(); + const executive = hasClaim(user, 'role:ceo') || hasClaim(user, 'role:vpo'); + const initials = (user?.name ?? '?') + .split(' ') + .map((p) => p[0]) + .slice(0, 2) + .join('') + .toUpperCase(); + + const item = (to: string, label: string, dim = false) => ( + `sitem${isActive ? ' on' : ''}${dim ? ' dim' : ''}`}> + {label} + + ); + + return ( +
+ +
+ +
+ +
+ ); +} diff --git a/apps/web/src/api.ts b/apps/web/src/api.ts new file mode 100644 index 0000000..8529c5b --- /dev/null +++ b/apps/web/src/api.ts @@ -0,0 +1,58 @@ +import type { UserClaims } from '@ubi/types'; + +const TOKEN_KEY = 'ubi.token'; +const USER_KEY = 'ubi.user'; + +export function getToken(): string | null { + return localStorage.getItem(TOKEN_KEY); +} + +export function getUser(): UserClaims | null { + const raw = localStorage.getItem(USER_KEY); + return raw ? (JSON.parse(raw) as UserClaims) : null; +} + +export function setSession(token: string, user: UserClaims): void { + localStorage.setItem(TOKEN_KEY, token); + localStorage.setItem(USER_KEY, JSON.stringify(user)); +} + +export function clearSession(): void { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(USER_KEY); +} + +export async function api(path: string, init?: RequestInit): Promise { + const token = getToken(); + const res = await fetch(`/api${path}`, { + ...init, + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...(init?.headers ?? {}), + }, + }); + if (res.status === 401) { + clearSession(); + window.location.assign('/login'); + throw new Error('session expired'); + } + if (!res.ok) { + const body = (await res.json().catch(() => null)) as { message?: string | string[] } | null; + const msg = body?.message; + throw new Error(Array.isArray(msg) ? msg.join(', ') : (msg ?? `${res.status} ${res.statusText}`)); + } + return (await res.json()) as T; +} + +export function hasClaim(user: UserClaims | null, claim: string): boolean { + return !!user && (user.claims.includes(claim) || user.claims.includes('role:admin')); +} + +/** Landing route by role: exec → ManCom, dept head → their dashboard, else work. */ +export function homeFor(claims: string[]): string { + if (claims.some((c) => ['role:ceo', 'role:vpo', 'role:admin'].includes(c))) return '/mancom'; + const dept = claims.find((c) => c.startsWith('dept:'))?.slice(5); + if (dept && claims.includes('role:manager')) return `/dept/${dept}`; + return '/approvals'; +} diff --git a/apps/web/src/charts.tsx b/apps/web/src/charts.tsx new file mode 100644 index 0000000..9756f0b --- /dev/null +++ b/apps/web/src/charts.tsx @@ -0,0 +1,130 @@ +import type { TrendPoint } from '@ubi/types'; + +/** Chart convention (design language): grey = plan, teal solid = actual, + * orange dashed = projected. Projection series renders only when `project`. */ +export function TrendChart({ data, project, unit }: { data: TrendPoint[]; project: boolean; unit: string }) { + const w = 460; + const h = 180; + const px = 38; + const py = 16; + const plotW = w - px - 10; + const plotH = h - py - 28; + + const values = data.flatMap((d) => [d.plan, d.actual, project ? d.projected : null]).filter((v): v is number => v != null); + const max = Math.max(...values, 1); + const x = (i: number) => px + (i / Math.max(data.length - 1, 1)) * plotW; + const y = (v: number) => py + plotH - (v / max) * plotH; + + const line = (pick: (d: TrendPoint) => number | null) => + data + .map((d, i) => ({ v: pick(d), i })) + .filter((p): p is { v: number; i: number } => p.v != null) + .map((p, idx) => `${idx === 0 ? 'M' : 'L'}${x(p.i).toFixed(1)},${y(p.v).toFixed(1)}`) + .join(' '); + + const lastActualIdx = data.reduce((acc, d, i) => (d.actual != null ? i : acc), -1); + // Bridge actual → projection so the dashed line continues the solid one. + const projSeries = data.map((d, i) => (i === lastActualIdx ? d.actual : project ? d.projected : null)); + const projPath = line((d) => projSeries[data.indexOf(d)]); + + return ( +
+ + + + 0 + {Math.round(max / 2)} + {Math.round(max)} + {lastActualIdx >= 0 && ( + <> + + {/* survey flag marks the station we've reached */} + + + TODAY + + + )} + d.plan)} fill="none" stroke="#9aa9b6" strokeWidth="2" /> + d.actual)} fill="none" stroke="#0e7490" strokeWidth="2.6" /> + {project && } + {data.map((d, i) => ( + + {d.period} + + ))} + +
+ plan + actual ({unit}) + {project && projected (trend)} +
+
+ ); +} + +export function BarsChart({ data, project, unit }: { data: TrendPoint[]; project: boolean; unit: string }) { + const w = 460; + const h = 180; + const px = 38; + const py = 16; + const plotW = w - px - 10; + const plotH = h - py - 28; + const shown = data.filter((d) => d.actual != null || (project && d.projected != null)); + const values = shown.map((d) => d.actual ?? d.projected ?? 0); + const max = Math.max(...values, 1); + const bw = Math.min(40, (plotW / Math.max(shown.length, 1)) * 0.62); + + return ( +
+ + + 0 + {Math.round(max)} + {shown.map((d, i) => { + const cx = px + ((i + 0.5) / shown.length) * plotW; + const v = d.actual ?? d.projected ?? 0; + const bh = (v / max) * plotH; + const isProj = d.actual == null; + return ( + + {isProj ? ( + + ) : ( + + )} + {d.period} + + ); + })} + +
+ reported ({unit}) + {project && projected} +
+
+ ); +} + +export function Sparkline({ points }: { points: number[] }) { + if (!points.length) return null; + const w = 64; + const h = 18; + const min = Math.min(...points); + const max = Math.max(...points); + const span = max - min || 1; + const path = points + .map((v, i) => `${i === 0 ? 'M' : 'L'}${(2 + (i / (points.length - 1)) * (w - 4)).toFixed(1)},${(h - 3 - ((v - min) / span) * (h - 6)).toFixed(1)}`) + .join(' '); + const rising = points[points.length - 1] >= points[0]; + return ( + + + + ); +} diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 0000000..59da7f4 --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,17 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App'; +import '@fontsource-variable/space-grotesk'; +import '@fontsource/ibm-plex-mono/400.css'; +import '@fontsource/ibm-plex-mono/600.css'; +import '@fontsource/ibm-plex-mono/700.css'; +import './styles.css'; + +createRoot(document.getElementById('root')!).render( + + + + + , +); diff --git a/apps/web/src/pages/Approvals.tsx b/apps/web/src/pages/Approvals.tsx new file mode 100644 index 0000000..51c6a47 --- /dev/null +++ b/apps/web/src/pages/Approvals.tsx @@ -0,0 +1,61 @@ +import { useEffect, useState } from 'react'; +import { api } from '../api'; +import { Loader, SheetBar, Stamp } from '../ui'; + +interface ApprovalRow { + id: string; + subjectType: string; + title: string; + status: string; + currentStep: number; + chain: Array<{ step: string; actorClaim: string; status: string; by: string | null }>; + createdBy: string | null; + createdAt: string; +} + +export default function Approvals() { + const [rows, setRows] = useState(null); + const [error, setError] = useState(null); + + const refresh = () => api('/approvals').then(setRows).catch((e: Error) => setError(e.message)); + useEffect(() => { void refresh(); }, []); + + async function act(id: string, action: 'approve' | 'return') { + await api(`/approvals/${id}/act`, { method: 'POST', body: JSON.stringify({ action }) }); + await refresh(); + } + + if (error) return
{error}
; + if (!rows) return ; + + return ( + <> + + {rows.length === 0 &&
Inbox zero — nothing waits on you.
} + {rows.map((a) => ( +
+
+ {a.subjectType} + {a.title} + by {a.createdBy} · {new Date(a.createdAt).toLocaleDateString()} +
+
+ {a.chain.map((s, i) => + s.status === 'approved' ? ( + {`${s.step} ✓`} + ) : i === a.currentStep ? ( + {`${s.step} · you`} + ) : ( + {s.step} + ), + )} +
+
+ + +
+
+ ))} + + ); +} diff --git a/apps/web/src/pages/Ask.tsx b/apps/web/src/pages/Ask.tsx new file mode 100644 index 0000000..7e8c554 --- /dev/null +++ b/apps/web/src/pages/Ask.tsx @@ -0,0 +1,91 @@ +import { FormEvent, useEffect, useRef, useState } from 'react'; +import type { AskAnswer } from '@ubi/types'; +import { api, getUser } from '../api'; +import { SheetBar, Type } from '../ui'; + +interface Turn { + who: 'me' | 'ai'; + text: string; + sources?: string[]; + /** Typewriter finished — sources/meta appear only after the ink dries. */ + typed: boolean; +} + +export default function Ask() { + const [turns, setTurns] = useState([]); + const [q, setQ] = useState(''); + const [busy, setBusy] = useState(false); + const user = getUser(); + const endRef = useRef(null); + + useEffect(() => { + endRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); + }, [turns, busy]); + + function markTyped(index: number) { + setTurns((t) => t.map((turn, i) => (i === index ? { ...turn, typed: true } : turn))); + } + + async function send(question: string) { + if (!question.trim() || busy) return; + setTurns((t) => [...t, { who: 'me', text: question, typed: true }]); + setQ(''); + setBusy(true); + try { + const res = await api('/ai/ask', { method: 'POST', body: JSON.stringify({ question }) }); + setTurns((t) => [...t, { who: 'ai', text: res.answer, sources: res.sources, typed: false }]); + } catch (err) { + setTurns((t) => [...t, { who: 'ai', text: `error: ${err instanceof Error ? err.message : 'failed'}`, typed: false }]); + } finally { + setBusy(false); + } + } + + function submit(e: FormEvent) { + e.preventDefault(); + void send(q); + } + + return ( + <> + +
+ {turns.length === 0 && ( +
+ Ask across the whole suite — answers cite the read-models they came from (Pillar 3). +
+ )} + {turns.map((t, i) => ( +
+ {t.who === 'ai' && !t.typed ? markTyped(i)} /> : t.text} + {t.typed && t.sources && ( +
+ {t.sources.map((s) => {s})} +
+ )} +
+ ))} + {busy && ( +
+ Consulting read-models... +
+ )} +
+
+
+ {["What's happening?", 'Why is Package 5 slipping?', 'Which billings are at risk this month?'].map((s) => ( + + ))} +
+
+ setQ(e.target.value)} + placeholder="Ask about any project or department…" + disabled={busy} + /> + +
+ + ); +} diff --git a/apps/web/src/pages/Dept.tsx b/apps/web/src/pages/Dept.tsx new file mode 100644 index 0000000..2cadaac --- /dev/null +++ b/apps/web/src/pages/Dept.tsx @@ -0,0 +1,132 @@ +import { useEffect, useState } from 'react'; +import { Link, useParams } from 'react-router-dom'; +import { api } from '../api'; +import { CctvThumb, KpiBand, Loader, SheetBar } from '../ui'; + +interface DeptDashboard { + slug: string; + label: string; + head: { email: string; name: string }; + kpis: Array<{ label: string; value: string; sub: string; tone: 'ok' | 'warn' | 'bad' | 'info' }>; + monitoring: { title: string; src: string; data: unknown }; + camera: { id: string; label: string; online: boolean; streamUrl: string | null }; +} + +/** Departments with a dedicated tools page (deep workflows). */ +const TOOLS: Record = { + engineering: { path: '/engineering', label: 'Open Engineering tools — projects · materials schedule' }, + procurement: { path: '/procurement', label: 'Open Procurement — materials intake · suppliers' }, + property: { path: '/property', label: 'Open Property tools — scan · re-custody · attest' }, + records: { path: '/records', label: 'Open Records tools — expiry · vehicle docs · physical index' }, + it: { path: '/it', label: 'Open IT control room — devices · PMS scheduler · DeskGuard · env' }, +}; + +function Value({ v }: { v: unknown }) { + if (v == null) return ; + if (typeof v === 'boolean') return {String(v)}; + if (typeof v === 'object') return {JSON.stringify(v)}; + return <>{String(v)}; +} + +function AutoTable({ rows }: { rows: Array> }) { + if (!rows.length) return
No records yet.
; + const keys = Object.keys(rows[0]).filter((k) => !['id', 'entity', 'createdBy', 'updatedAt', 'deletedAt'].includes(k)); + return ( + + {keys.map((k) => )} + + {rows.map((r, i) => ( + {keys.map((k) => )} + ))} + +
{k}
+ ); +} + +function Monitoring({ data }: { data: unknown }) { + if (Array.isArray(data)) return >} />; + if (data && typeof data === 'object') { + return ( + + + {Object.entries(data as Record).map(([k, v]) => ( + + + + + ))} + +
{k}{Array.isArray(v) ? >} /> : }
+ ); + } + return
No data.
; +} + +/** Standard department dashboard — same shape for every department: + * uniform KPI band · field monitoring (the dept's read-model) · CCTV thumbnail. */ +export default function Dept() { + const { slug = '' } = useParams(); + const [dash, setDash] = useState(null); + const [tickets, setTickets] = useState> | null>(null); + const [error, setError] = useState(null); + + useEffect(() => { + setDash(null); + setTickets(null); + setError(null); + api(`/dept/${slug}/dashboard`).then(setDash).catch((e: Error) => setError(e.message)); + if (slug === 'it') api>>('/tickets?source=it').then(setTickets).catch(() => {}); + }, [slug]); + + if (error) return <>
{error}
; + if (!dash) return <>; + + return ( + <> + + + {TOOLS[slug] && ( +
+ {TOOLS[slug].label} → +
+ )} + + + +
+
+
+ Field monitoring — {dash.monitoring.title} + SRC · {dash.monitoring.src} +
+ +
+ +
+
+ CCTV + {dash.camera.id} +
+ +
+ {dash.camera.online ? 'Live thumbnail from NVR.' : 'Set CCTV_BASE_URL to link the Hikvision NVR snapshot.'} +
+
+
+ + {slug === 'it' && tickets && ( +
+
+ Helpdesk queue + SRC · ticketing (it) · public form feeds this +
+ +
+ )} + + ); +} diff --git a/apps/web/src/pages/Docs.tsx b/apps/web/src/pages/Docs.tsx new file mode 100644 index 0000000..1f2ffbc --- /dev/null +++ b/apps/web/src/pages/Docs.tsx @@ -0,0 +1,141 @@ +import { FormEvent, useEffect, useState } from 'react'; +import type { TrackedDocument, WhereIsAnswer } from '@ubi/types'; +import { api } from '../api'; +import { SheetBar } from '../ui'; + +export default function Docs() { + const [docs, setDocs] = useState([]); + const [code, setCode] = useState(''); + const [answer, setAnswer] = useState(null); + const [error, setError] = useState(null); + const [to, setTo] = useState(''); + const [showNew, setShowNew] = useState(false); + const [nw, setNw] = useState({ type: 'Transmittal', title: '', location: '' }); + + const refresh = () => api('/docs').then(setDocs).catch((e: Error) => setError(e.message)); + useEffect(() => { void refresh(); }, []); + + async function lookup(c: string) { + setError(null); + try { + setAnswer(await api(`/docs/where/${encodeURIComponent(c)}`)); + } catch (err) { + setAnswer(null); + setError(err instanceof Error ? err.message : 'lookup failed'); + } + } + + function submit(e: FormEvent) { + e.preventDefault(); + void lookup(code); + } + + async function createDoc(e: FormEvent) { + e.preventDefault(); + if (!nw.title.trim()) return; + try { + const doc = await api('/docs', { method: 'POST', body: JSON.stringify(nw) }); + setShowNew(false); + setNw({ type: 'Transmittal', title: '', location: '' }); + await refresh(); + setCode(doc.code); + await lookup(doc.code); // open it so you can transmit immediately + } catch (err) { + setError(err instanceof Error ? err.message : 'create failed'); + } + } + + async function transmit() { + if (!answer || !to.trim()) return; + await api(`/docs/${answer.document.code}/transmit`, { method: 'POST', body: JSON.stringify({ to }) }); + setTo(''); + await lookup(answer.document.code); + await refresh(); + } + + async function receive() { + if (!answer) return; + await api(`/docs/${answer.document.code}/receive`, { method: 'POST', body: JSON.stringify({}) }); + await lookup(answer.document.code); + await refresh(); + } + + return ( + <> + + +
+
+ setCode(e.target.value)} placeholder="Where is… (scan or type a code, e.g. DOC-2026-00001)" /> + +
+ +
+ + {showNew && ( +
+
Register a new document / transmittalgets a QR code + tracking id
+
+ + setNw({ ...nw, title: e.target.value })} placeholder="Title, e.g. Billing No. 8 — attachments batch (PKG-02)" required /> + setNw({ ...nw, location: e.target.value })} placeholder="Origin location (optional)" /> + +
+
You become the current holder; then transmit it to the next department below.
+
+ )} + + {error &&
{error}
} + + {answer && ( +
+
+ {answer.document.title} + {answer.document.code} + {answer.document.status} + holder: {answer.document.currentHolder} · {answer.document.currentLocation} +
+ + + + {answer.history.map((m) => ( + + + + + + + ))} + +
WhenMovementByNote
{new Date(m.at).toLocaleString()}{m.kind}: {m.from}{m.to}{m.by}{m.note ?? '—'}
+
+ setTo(e.target.value)} placeholder="transmit to… (e.g. Finance — Cashier)" /> + + +
+
+ )} + +
+
Recent documentsSRC · doc-tracking
+ + + + {docs.map((d) => ( + { setCode(d.code); void lookup(d.code); }} style={{ cursor: 'pointer' }}> + + + + + + ))} + +
CodeTitleHolderStatus
{d.code}{d.title}{d.currentHolder}{d.status}
+
+ + ); +} diff --git a/apps/web/src/pages/Engineering.tsx b/apps/web/src/pages/Engineering.tsx new file mode 100644 index 0000000..b9999d5 --- /dev/null +++ b/apps/web/src/pages/Engineering.tsx @@ -0,0 +1,113 @@ +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import type { MaterialsSchedule, PayItem, Project } from '@ubi/types'; +import { api, getUser, hasClaim } from '../api'; +import { SheetBar } from '../ui'; + +export default function Engineering() { + const [projects, setProjects] = useState([]); + const [payItems, setPayItems] = useState([]); + const [schedules, setSchedules] = useState([]); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + const user = getUser(); + const canPrepare = hasClaim(user, 'role:pe') || hasClaim(user, 'role:pm'); + + const refresh = () => + Promise.all([ + api('/engineering/projects').then(setProjects), + api('/engineering/pay-items').then(setPayItems), + api('/engineering/materials-schedules').then(setSchedules), + ]).catch((e: Error) => setError(e.message)); + + useEffect(() => { void refresh(); }, []); + + /** CEO-priority flow: AI prefills, PE adjusts, submit → PM → VPO. */ + async function draftThisWeek(projectId: string) { + setBusy(true); + try { + const lines = await api(`/engineering/materials-schedules/prefill/${projectId}`); + const weekOf = new Date().toISOString().slice(0, 10); + const created = await api('/engineering/materials-schedules', { + method: 'POST', + body: JSON.stringify({ projectId, weekOf, lines }), + }); + await api(`/engineering/materials-schedules/${created.id}/submit`, { method: 'POST', body: JSON.stringify({}) }); + await refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : 'failed'); + } finally { + setBusy(false); + } + } + + return ( + <> + +
+ Quantity — weekly entry → + Quantity head dashboard → + Projection & materials → + Equipment schedule → Omega + Procurement intake → +
+ {error &&
{error}
} + +
+
+
Projects (DPWH packages)
+ + {canPrepare && + + {projects.map((p) => ( + + + + + + {canPrepare && ( + + )} + + ))} + +
CodeNameContractSlip}
{p.code}{p.name}{p.contractNo}{p.slippagePct}% + +
+
+ +
+
Pay-item library (DPWH Blue Book)
+ + + + {payItems.map((i) => ( + + ))} + +
ItemDescriptionUnit
{i.itemNo}{i.description}{i.unit}
+
+
+ +
+
Weekly materials schedulesPE → PM → VPO → Procurement
+ {schedules.length === 0 &&
None yet — draft one from a project row (AI prefills the lines).
} + + + {schedules.map((s) => ( + + + + + + ))} + +
wk of {s.weekOf} · {s.lines.length} lines ({s.lines.filter((l) => l.aiSuggested).length} AI-suggested){s.preparedBy} + {s.status} +
+
+ + ); +} diff --git a/apps/web/src/pages/Equipment.tsx b/apps/web/src/pages/Equipment.tsx new file mode 100644 index 0000000..bd14752 --- /dev/null +++ b/apps/web/src/pages/Equipment.tsx @@ -0,0 +1,106 @@ +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import type { Project } from '@ubi/types'; +import { api } from '../api'; +import { Loader, SheetBar, Stamp } from '../ui'; + +interface Line { activity: string; equipment: string; qty: number; unit: string; fromDate: string; toDate: string } +interface Schedule { id: string; projectId: string; weekOf: string; lines: Line[]; status: 'draft' | 'submitted' | 'pushed'; omegaRef: string | null; pushedAt: string | null } + +const EQUIP = ['Transit mixer', 'Vibratory roller', 'Dump truck', 'Backhoe loader', 'Motor grader', 'Crane', 'Concrete vibrator', 'Generator']; +const blank = (): Line => ({ activity: '', equipment: EQUIP[0], qty: 1, unit: 'units', fromDate: '', toDate: '' }); + +/** Weekly machinery/activity schedule → pushed to Omega (OAEC) via API. */ +export default function Equipment() { + const [projects, setProjects] = useState([]); + const [schedules, setSchedules] = useState(null); + const [error, setError] = useState(null); + const [pid, setPid] = useState(''); + const [weekOf, setWeekOf] = useState(''); + const [lines, setLines] = useState([blank()]); + + const refresh = () => api('/equipment/schedules').then(setSchedules).catch((e: Error) => setError(e.message)); + useEffect(() => { + api('/engineering/projects').then((p) => { setProjects(p); if (p[0]) setPid(p[0].id); }).catch(() => {}); + void refresh(); + }, []); + + function setLine(i: number, patch: Partial) { setLines((ls) => ls.map((l, j) => (j === i ? { ...l, ...patch } : l))); } + + async function create() { + const clean = lines.filter((l) => l.activity && l.equipment); + if (!pid || !weekOf || !clean.length) return; + const created = await api('/equipment/schedules', { method: 'POST', body: JSON.stringify({ projectId: pid, weekOf, lines: clean }) }); + await api(`/equipment/schedules/${created.id}/submit`, { method: 'POST', body: JSON.stringify({}) }); + setLines([blank()]); setWeekOf(''); + await refresh(); + } + + async function push(id: string) { + await api(`/equipment/schedules/${id}/push`, { method: 'POST', body: JSON.stringify({}) }); + await refresh(); + } + + const code = (id: string) => projects.find((p) => p.id === id)?.code ?? id.slice(0, 6); + if (error) return <>
{error}
; + if (!schedules) return <>; + + return ( + <> + +
Quantity weekly entry →
+ +
+
New weekly equipment schedule→ OAEC (Omega fleet)
+
+ + +
+ + + + {lines.map((l, i) => ( + + + + + + + + + + ))} + +
Activity (requires machinery)EquipmentQtyUnitFromTo
setLine(i, { activity: e.target.value })} placeholder="e.g. PCCP pouring STA 4+200" /> setLine(i, { qty: Number(e.target.value) })} /> setLine(i, { unit: e.target.value })} /> setLine(i, { fromDate: e.target.value })} /> setLine(i, { toDate: e.target.value })} />{lines.length > 1 && }
+
+ + +
+
+ +
+
SchedulesSRC · engineering.equipment-schedule
+ {schedules.length === 0 ?
None yet.
: schedules.map((s) => ( +
+
+ {code(s.projectId)} · wk {s.weekOf} + {s.lines.length} activities + {s.status === 'pushed' ? pushed to omega : s.status === 'submitted' ? submitted : draft} + {s.omegaRef && OAEC ref {s.omegaRef}} + {s.status !== 'pushed' && } +
+ + + {s.lines.map((l, i) => ( + + ))} + +
{l.activity}{l.equipment} ×{l.qty} {l.unit}{l.fromDate} → {l.toDate}
+
+ ))} +
+ + ); +} diff --git a/apps/web/src/pages/Fleet.tsx b/apps/web/src/pages/Fleet.tsx new file mode 100644 index 0000000..38ffb78 --- /dev/null +++ b/apps/web/src/pages/Fleet.tsx @@ -0,0 +1,170 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { MapContainer, TileLayer, CircleMarker, Popup, useMap } from 'react-leaflet'; +import 'leaflet/dist/leaflet.css'; +import { api } from '../api'; +import { Loader, SheetBar } from '../ui'; + +interface LiveVehicle { + id: string; registration: string; name: string | null; lat: number | null; lng: number | null; + speed: number | null; heading: number | null; ignition: boolean | null; + odometerKm: number | null; fuelPct: number | null; driver: string | null; + address: string | null; at: string | null; demo?: boolean; +} +type Row = Record; +type Tab = 'live' | 'equipment' | 'trips' | 'events' | 'alerts' | 'geofences' | 'maintenance'; + +const TABS: { key: Tab; label: string }[] = [ + { key: 'live', label: 'Live map' }, + { key: 'equipment', label: 'Equipment' }, + { key: 'trips', label: 'Trips' }, + { key: 'events', label: 'Driver events' }, + { key: 'alerts', label: 'Alerts' }, + { key: 'geofences', label: 'Geofences' }, + { key: 'maintenance', label: 'Maintenance' }, +]; +const COLS: Partial> = { + equipment: ['unitNo', 'plate', 'equipmentType', 'make', 'model', 'year', 'status', 'assignedProject'], + trips: ['registration', 'start_timestamp', 'end_timestamp', 'trip_distance', 'trip_duration', 'start_location', 'end_location'], + events: ['registration', 'eventType', 'eventDescription', 'occurredAt', 'speedKmh', 'address'], + alerts: ['registration', 'alertType', 'message', 'occurredAt', 'acknowledged'], + geofences: ['name', 'description', 'position_description', 'colour'], + maintenance: ['registration', 'name', 'model', 'under_maintenance', 'licence_expiry'], +}; + +function FitBounds({ pts }: { pts: [number, number][] }) { + const map = useMap(); + useEffect(() => { if (pts.length) map.fitBounds(pts, { padding: [40, 40], maxZoom: 11 }); }, [pts, map]); + return null; +} + +function Val({ v }: { v: unknown }) { + if (v == null || v === '') return ; + if (typeof v === 'boolean') return {v ? 'yes' : 'no'}; + if (typeof v === 'object') return {JSON.stringify(v).slice(0, 40)}; + return <>{String(v)}; +} +function DataTable({ rows, cols }: { rows: Row[]; cols?: string[] }) { + if (!rows.length) return
No records in this window.
; + const keys = (cols ?? Object.keys(rows[0])).filter((k) => k in rows[0]); + const show = keys.length ? keys : Object.keys(rows[0]).slice(0, 7); + return ( + + {show.map((k) => )} + {rows.map((r, i) => {show.map((k) => )})} +
{k.replace(/_/g, ' ')}
+ ); +} + +export default function Fleet() { + const [tab, setTab] = useState('live'); + const [vehicles, setVehicles] = useState(null); + const [cfg, setCfg] = useState<{ configured: boolean; region: string } | null>(null); + const [sel, setSel] = useState(null); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const timer = useRef(); + + const loadLive = () => api('/fleet/live').then(setVehicles).catch((e: Error) => setError(e.message)); + + useEffect(() => { + api<{ configured: boolean; region: string }>('/fleet/status').then(setCfg).catch(() => {}); + void loadLive(); + // Server caches at 10s — client polls at 5s for near-real-time feel. + timer.current = window.setInterval(loadLive, 5000); + return () => window.clearInterval(timer.current); + }, []); + + useEffect(() => { + if (tab === 'live') return; + setData(null); + const path = + tab === 'alerts' ? '/fleet/notifications' : + tab === 'equipment' ? '/omega/equipment' : + `/fleet/${tab}`; + api(path).then(setData).catch((e: Error) => setError(e.message)); + }, [tab]); + + const located = useMemo(() => (vehicles ?? []).filter((v) => v.lat != null && v.lng != null), [vehicles]); + const pts = useMemo(() => located.map((v) => [v.lat!, v.lng!] as [number, number]), [located]); + const center: [number, number] = pts[0] ?? [8.0, 125.0]; + + async function locate(id: string) { + await api(`/fleet/vehicles/${id}/locate`, { method: 'POST', body: '{}' }).catch(() => {}); + setTimeout(loadLive, 1500); + } + + if (error) return <>
{error}
; + + return ( + <> + + {cfg && !cfg.configured && ( +
Demo fleet shown. Set CARTRACK_USER/CARTRACK_PASS (region {cfg.region}) for live GPS.
+ )} + +
+ {TABS.map((t) => )} +
+ + {tab === 'live' ? ( + !vehicles ? : ( +
+
+ + + + {located.map((v) => ( + 0 ? '#15803d' : '#b45309', fillOpacity: 0.9 }} + eventHandlers={{ click: () => setSel(v.id) }}> + + {v.registration} {v.name ? `· ${v.name}` : ''}
+ {v.ignition && (v.speed ?? 0) > 0 ? '🟢 moving' : '🔴 stopped'} · {v.speed ?? 0} km/h
+ Odometer: {v.odometerKm?.toLocaleString() ?? '—'} km
+ Fuel: {v.fuelPct != null ? `${v.fuelPct.toFixed(1)}%` : '—'}{v.fuelPct != null && v.fuelPct < 20 ? ' ⚠️ LOW' : ''}
+ {v.driver ? <>{v.driver}
: null} + {v.address ?? `${v.lat!.toFixed(4)}, ${v.lng!.toFixed(4)}`}
+ {v.at ? new Date(v.at).toLocaleString() : ''} +
+
+ ))} +
+
+
+
VehiclesSRC · fleet.live
+
+ + + + {(vehicles ?? []).map((v) => { + const lowFuel = v.fuelPct != null && v.fuelPct < 20; + return ( + setSel(v.id)} style={{ cursor: 'pointer', background: sel === v.id ? 'rgba(194,65,12,0.06)' : undefined }}> + + + + + + + ); + })} + +
RegStatuskm/hFuel
{v.registration}
{(v.address ?? '').slice(0, 30)}
0 ? 'st-ok' : 'st-warn'}`}>{v.ignition && (v.speed ?? 0) > 0 ? 'moving' : 'stopped'}{v.speed ?? 0} + {v.fuelPct != null + ? {v.fuelPct.toFixed(0)}% + : } +
+
+
Client refresh 5s · server cache 10s · {located.length}/{vehicles.length} located
+
+
+ ) + ) : ( +
+
{TABS.find((t) => t.key === tab)!.label}SRC · fleet.{tab}
+ {!data ? : } +
+ )} + + ); +} diff --git a/apps/web/src/pages/Helpdesk.tsx b/apps/web/src/pages/Helpdesk.tsx new file mode 100644 index 0000000..d9a8742 --- /dev/null +++ b/apps/web/src/pages/Helpdesk.tsx @@ -0,0 +1,160 @@ +import { FormEvent, useEffect, useRef, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { Stamp } from '../ui'; + +const SITE_KEY = import.meta.env.VITE_TURNSTILE_SITE_KEY as string | undefined; + +declare global { + interface Window { + turnstile?: { + render: ( + el: HTMLElement, + opts: { sitekey: string; callback: (token: string) => void; 'expired-callback'?: () => void }, + ) => string; + }; + } +} + +/** Public IT helpdesk — no sign-in; Cloudflare Turnstile stands at the gate. */ +export default function Helpdesk() { + const [form, setForm] = useState({ name: '', email: '', location: '', category: 'Hardware', title: '', body: '' }); + const [token, setToken] = useState(null); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + const [done, setDone] = useState<{ id: string } | null>(null); + const slot = useRef(null); + + useEffect(() => { + if (!SITE_KEY || !slot.current) return; + const exists = document.getElementById('cf-turnstile-js'); + if (!exists) { + const s = document.createElement('script'); + s.id = 'cf-turnstile-js'; + s.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; + s.async = true; + document.head.appendChild(s); + } + const timer = setInterval(() => { + if (window.turnstile && slot.current && !slot.current.hasChildNodes()) { + window.turnstile.render(slot.current, { + sitekey: SITE_KEY, + callback: setToken, + 'expired-callback': () => setToken(null), + }); + clearInterval(timer); + } + }, 250); + return () => clearInterval(timer); + }, []); + + const set = (k: keyof typeof form) => (e: { target: { value: string } }) => + setForm((f) => ({ ...f, [k]: e.target.value })); + + async function submit(e: FormEvent) { + e.preventDefault(); + if (SITE_KEY && !token) { + setError('please complete the captcha first'); + return; + } + setBusy(true); + setError(null); + try { + const res = await fetch('/api/public/helpdesk', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: form.name, + email: form.email || undefined, + location: form.location, + category: form.category, + title: form.title, + body: form.body, + turnstileToken: token ?? undefined, + }), + }); + const data = (await res.json()) as { id?: string; message?: string | string[] }; + if (!res.ok) throw new Error(Array.isArray(data.message) ? data.message.join(', ') : (data.message ?? 'failed')); + setDone({ id: data.id! }); + } catch (err) { + setError(err instanceof Error ? err.message : 'submit failed'); + } finally { + setBusy(false); + } + } + + return ( +
+
+
PUBLIC FORM
NO SIGN-IN
+
+
+
U
+
+

IT Helpdesk

+
Report an issue · Ulticon Builders · Omega Asia
+
+
+ + {done ? ( +
+ Ticket logged +
+ Reference: TKT-{done.id.slice(0, 8).toUpperCase()} +
+ IT receives this in the shared queue — keep the reference for follow-ups. +
+
+ +
+ ) : ( + <> + {error &&
{error}
} +
+
+ + + + +
+ + +
+ {SITE_KEY ?
: captcha off — set VITE_TURNSTILE_SITE_KEY + TURNSTILE_SECRET_KEY} +
+ + + + )} + +
+
SheetHD-01
+
QueueIT · SHARED
+
Gate{SITE_KEY ? 'TURNSTILE' : 'DEV'}
+
RevA
+
+
+
+ Staff with accounts: sign in to track tickets, approvals and documents. +
+
+
+ ); +} diff --git a/apps/web/src/pages/Insights.tsx b/apps/web/src/pages/Insights.tsx new file mode 100644 index 0000000..fa3ed91 --- /dev/null +++ b/apps/web/src/pages/Insights.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react'; +import type { Insight } from '@ubi/types'; +import { api } from '../api'; +import { Loader, SheetBar, Stamp } from '../ui'; + +export default function Insights() { + const [items, setItems] = useState(null); + const [error, setError] = useState(null); + const [filter, setFilter] = useState('all'); + + useEffect(() => { + api('/ceo/insights').then(setItems).catch((e: Error) => setError(e.message)); + }, []); + + if (error) return
{error}
; + if (!items) return ; + const shown = items.filter((i) => filter === 'all' || i.severity === filter); + const counts = (sev: string) => items.filter((i) => i.severity === sev).length; + + return ( + <> + +
+ {(['all', 'critical', 'warning', 'positive'] as const).map((f) => ( + + ))} +
+ {shown.map((ins) => ( +
+
+ + {ins.severity} + + {ins.title} +
+

{ins.body}

+
+ {ins.evidence.map((e) => {e})} +
+
+ {ins.actions.map((a) => ( + + ))} +
+
+ generated {new Date(ins.generatedAt).toLocaleString()} by {ins.generatedBy} +
+
+ ))} + + ); +} diff --git a/apps/web/src/pages/It.tsx b/apps/web/src/pages/It.tsx new file mode 100644 index 0000000..5b35c56 --- /dev/null +++ b/apps/web/src/pages/It.tsx @@ -0,0 +1,100 @@ +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { api } from '../api'; +import { Loader, SheetBar } from '../ui'; + +interface Device { id: string; name: string; kind: string; ip: string; status: string; detail: string | null } +interface Pms { id: string; assetQr: string; assetType: string; quarter: string; dueOn: string; status: string } +interface Env { id: string; sensor: string; tempC: number; humidity: number | null } +interface Desk { id: string; person: string; topApp: string; kpiActivity: string; keystrokes: number; mouse: number; activeMinutes: number } + +const dtone = (s: string) => (s === 'up' ? 'st-ok' : s === 'warn' ? 'st-warn' : 'st-bad'); + +export default function It() { + const [devices, setDevices] = useState([]); + const [pms, setPms] = useState([]); + const [env, setEnv] = useState([]); + const [desk, setDesk] = useState([]); + const [consent, setConsent] = useState(''); + const [ready, setReady] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + Promise.all([ + api('/it/devices').then(setDevices), + api('/it/pms').then(setPms), + api('/it/env').then(setEnv), + api('/it/deskguard').then(setDesk), + api<{ consent: string }>('/it/deskguard/policy').then((p) => setConsent(p.consent)), + ]).then(() => setReady(true)).catch((e: Error) => setError(e.message)); + }, []); + + if (error) return <>
{error}
; + if (!ready) return <>; + + return ( + <> + + +
+
+
Monitored devicesSRC · it.devices
+ + + + {devices.map((d) => ( + + + + + + ))} + +
DeviceKindIPStatus
{d.name}
{d.detail}
{d.kind}{d.ip}{d.status}
+
+ +
+
EnvironmentalSRC · it.env
+ + + + {env.map((e) => ( + + + + + + ))} + +
SensorTemp °CHumidity
{e.sensor} 27 ? 'st-bad' : e.tempC > 25 ? 'st-warn' : 'st-ok'}`}>{e.tempC}°{e.humidity != null ? `${e.humidity}%` : '—'}
+
+
+ +
+
Preventive maintenance (PMS)SRC · it.pms
+
+ {pms.length} scheduled · {pms.filter((p) => p.status === 'done').length} done across quarters + Open PMS scheduler — Q1–Q4 · list · kanban · calendar → +
+
+ +
+
DeskGuard v2 — activity (self-report)SRC · it.deskguard
+
+ ⚠ {consent} +
+ + + + {desk.map((d) => ( + + + + + ))} + +
PersonTop appKPI activityKeystrokesMouseActive min
{d.person}{d.topApp}{d.kpiActivity}{d.keystrokes.toLocaleString()}{d.mouse.toLocaleString()}{d.activeMinutes}
+
+ + ); +} diff --git a/apps/web/src/pages/Login.tsx b/apps/web/src/pages/Login.tsx new file mode 100644 index 0000000..70f2418 --- /dev/null +++ b/apps/web/src/pages/Login.tsx @@ -0,0 +1,120 @@ +import { FormEvent, useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import type { UserClaims } from '@ubi/types'; +import { api, homeFor, setSession } from '../api'; + +const EXEC = [ + ['ceo@ubi.ph', 'ceo123', 'CEO'], + ['vpo@ubi.ph', 'vpo123', 'VPO'], + ['admin@ubi.ph', 'admin123', 'Admin'], + ['pm@ubi.ph', 'pm123', 'PM'], + ['pe@ubi.ph', 'pe123', 'PE (field)'], +] as const; + +const DEPT_HEADS = [ + 'engineering', 'procurement', 'operations', 'survey', 'mqc', 'audit', + 'it', 'records', 'clinic', 'admin', 'hr', 'property', 'finance', +]; + +// Real data now: demo tap-to-login is shown ONLY in local dev. No env override — +// a production build always strips it, regardless of any box-side VITE_* flag. +const SHOW_DEMOS = import.meta.env.DEV; + +export default function Login() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + const navigate = useNavigate(); + + function fill(em: string, pw: string) { + setEmail(em); + setPassword(pw); + setError(null); + } + + async function submit(e: FormEvent) { + e.preventDefault(); + setBusy(true); + setError(null); + try { + const res = await api<{ token: string; user: UserClaims }>('/auth/login', { + method: 'POST', + body: JSON.stringify({ email, password }), + }); + setSession(res.token, res.user); + navigate(homeFor(res.user.claims)); + } catch (err) { + setError(err instanceof Error ? err.message : 'login failed'); + } finally { + setBusy(false); + } + } + + return ( +
+
+
FIELD COPY
REV A
+
+
+
U
+
+

UBI Construction Suite

+
Ulticon Builders · Omega Asia
+
+
+ {error &&
{error}
} +
+ + + + +
+
+
SheetLOGIN-01
+
Drawn byUBI / IT
+
ScaleNTS
+
RevA
+
+
+ {SHOW_DEMOS && ( +
+
Tap to autofill a demo login
+
+ {EXEC.map(([em, pw, label]) => ( + + ))} +
+
+ +
+
+ )} + +
+ Offline-ready: after the first online sign-in this device keeps a secure session for field use. +
+ IT problem, no account? File a helpdesk ticket — no sign-in needed → +
+
+
+
+ ); +} diff --git a/apps/web/src/pages/ManCom.tsx b/apps/web/src/pages/ManCom.tsx new file mode 100644 index 0000000..128d7fe --- /dev/null +++ b/apps/web/src/pages/ManCom.tsx @@ -0,0 +1,235 @@ +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import type { ManComSnapshot } from '@ubi/types'; +import { api } from '../api'; +import { BarsChart, Sparkline, TrendChart } from '../charts'; +import { Chainage, Loader, SheetBar } from '../ui'; + +const peso = (centavos: number) => `₱${(centavos / 100 / 1_000_000).toFixed(1)}M`; + +function FleetCard() { + const [s, setS] = useState<{ total: number; moving: number; stopped: number; noFix: number; configured: boolean } | null>(null); + useEffect(() => { api('/fleet/summary').then(setS).catch(() => {}); }, []); + if (!s) return null; + return ( +
+
Fleet (Cartrack) map →
+
+ {s.total} vehicles + {s.moving} moving + {s.stopped} stopped + {s.noFix > 0 && {s.noFix} no-fix} +
+ {!s.configured &&
demo — set Cartrack creds for live
} +
+ ); +} + +export default function ManCom() { + const [snap, setSnap] = useState(null); + const [error, setError] = useState(null); + const [project, setProject] = useState(() => localStorage.getItem('ubi.project') !== 'off'); + + useEffect(() => { + api('/ceo/mancom').then(setSnap).catch((e: Error) => setError(e.message)); + }, []); + + function toggle() { + setProject((p) => { + localStorage.setItem('ubi.project', p ? 'off' : 'on'); + return !p; + }); + } + + if (error) return
{error}
; + if (!snap) return ; + const k = snap.kpis; + + return ( + <> + +
+ + Project trends forward + +
+ + live · as of {new Date(snap.asOf).toLocaleTimeString()} + +
+
+ +
+
+
+
+
Active projects
+
+ {k.activeProjects} {k.flaggedProjects} ⚑ +
+
DPWH packages · flags pre-vetted by Operations
+
+
+
Billings — month
+
+ {peso(k.billingsMonthCentavos)} ▲ {k.billingsDeltaPct}% +
+ +
+
+
Accomplishment
+
+ {(k.accomplishmentPct * 100).toFixed(1)}% + ₱{(k.accomplishmentMonth / 1e6).toFixed(1)}M mo +
+
in-house, from QE weekly entries
+
+
+
Avg slippage
+
+ {k.avgSlippagePct}% + {project && k.projectedSlippagePct != null && ( + → {k.projectedSlippagePct}% next mo. + )} +
+
across active packages
+
+
+
Open exceptions
+
+ {k.openExceptions} {k.escalatedExceptions} escalated +
+
Audit investigation tracker
+
+
+ +
+
+
+ Accomplishment vs plan — cumulative ₱M + SRC · engineering.swa +
+ +
+
+
+ Survey volumes — monthly m³ ×1000 + SRC · survey.volumes +
+ +
+
+ +
+
+
+ Flagged projects + SRC · operations.health +
+ + + + + + {snap.flagged.map((f) => ( + + + + + + + + ))} + +
ProjectPE / PMSlipTrendProj.
{f.projectCode} · {f.projectName}{f.pe} / {f.pm}{f.slippagePct}% + {project && f.projectedPct != null ? ( + + {f.projectedPct}% + + ) : ( + + )} +
+
+ +
+
+ Quantity — top accomplishment this month + SRC · engineering.quantity +
+ + + + {snap.quantityTop.map((q) => ( + + + + + + + ))} + +
ProjectPETo-dateThis month
{q.code}{q.pe}{(q.pct * 100).toFixed(0)}%₱{(q.thisMonth / 1e6).toFixed(2)}M
+
+ +
+
+ Department report feed + SRC · *.report-status +
+ + + {snap.reportFeed.map((r) => ( + + + + + ))} + +
{r.module} — {r.report} + + {r.detail ?? r.status} + +
+
+
+
+ +
+
+ AI Insights view all → +
+ {snap.topInsights.map((ins) => ( +
+
● {ins.severity.toUpperCase()}
+

{ins.title}. {ins.body.slice(0, 110)}{ins.body.length > 110 ? '…' : ''}

+
+ {ins.evidence.slice(0, 3).map((e) => {e})} +
+
+ ))} + +
+
Ask AI
+ + “What's happening?” → + +
+
+
+ + ); +} diff --git a/apps/web/src/pages/Mqc.tsx b/apps/web/src/pages/Mqc.tsx new file mode 100644 index 0000000..59a153c --- /dev/null +++ b/apps/web/src/pages/Mqc.tsx @@ -0,0 +1,310 @@ +import { FormEvent, useEffect, useState } from 'react'; +import { api } from '../api'; +import { Loader, Stamp, SheetBar } from '../ui'; + +interface QcTest { + id: string; + projectId: string; + payItemNo: string; + testType: string; + result: string; + certId: string | null; + labRef: string | null; + testDate: string | null; + billingRef: string | null; +} + +interface PourLog { + id: string; + station: string | null; + pourDate: string; + mixDesign: string | null; + volumeM3: number; + slumpMm: number | null; + airContentPct: number | null; +} + +interface DpwhRule { test: string; per: string; qty: number } + +const PROJECTS = ['PKG-02', 'PKG-05', 'PKG-11', 'PKG-03', 'PKG-07', 'PKG-09']; +const PAY_ITEMS = ['311(1)a', '311(1)b', '311(1)c', '200', '201', '301']; +const RESULTS = ['pending', 'passed', 'failed']; + +function stColor(r: string): 'ok' | 'warn' | 'bad' { + return r === 'passed' ? 'ok' : r === 'failed' ? 'bad' : 'warn'; +} + +export default function Mqc() { + const [project, setProject] = useState('PKG-02'); + const [payItem, setPayItem] = useState('311(1)c'); + const [tests, setTests] = useState(null); + const [pours, setPours] = useState(null); + const [rules, setRules] = useState(null); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + const [tab, setTab] = useState<'tests' | 'pours'>('tests'); + + const [testForm, setTestForm] = useState({ + payItemNo: '311(1)c', testType: 'Concrete beam (7-day)', result: 'pending', + billingRef: '', certId: '', labRef: '', testDate: '', + }); + const [pourForm, setPourForm] = useState({ + station: '', pourDate: new Date().toISOString().slice(0, 10), + mixDesign: 'Class A 40.7 MPa', volumeM3: '', slumpMm: '', airContentPct: '', + payItemNo: '311(1)c', + }); + + const refresh = () => + Promise.all([ + api(`/mqc/projects/${project}/tests`).then(setTests), + api(`/mqc/projects/${project}/pours`).then(setPours), + api<{ requirements: DpwhRule[] }>(`/mqc/rules/${encodeURIComponent(payItem)}`).then((r) => setRules(r.requirements)), + ]).catch((e: Error) => setError(e.message)); + + useEffect(() => { void refresh(); }, [project]); + + useEffect(() => { + api<{ requirements: DpwhRule[] }>(`/mqc/rules/${encodeURIComponent(payItem)}`) + .then((r) => setRules(r.requirements)) + .catch(() => {}); + }, [payItem]); + + async function submitTest(e: FormEvent) { + e.preventDefault(); + setSubmitting(true); + try { + await api(`/mqc/projects/${project}/tests`, { + method: 'POST', + body: JSON.stringify(testForm), + }); + setTestForm({ payItemNo: '311(1)c', testType: 'Concrete beam (7-day)', result: 'pending', billingRef: '', certId: '', labRef: '', testDate: '' }); + setTests(null); + await api(`/mqc/projects/${project}/tests`).then(setTests); + } catch (e2) { setError((e2 as Error).message); } + finally { setSubmitting(false); } + } + + async function submitPour(e: FormEvent) { + e.preventDefault(); + setSubmitting(true); + try { + await api(`/mqc/projects/${project}/pours`, { + method: 'POST', + body: JSON.stringify({ ...pourForm, volumeM3: Number(pourForm.volumeM3), slumpMm: pourForm.slumpMm ? Number(pourForm.slumpMm) : null, airContentPct: pourForm.airContentPct ? Number(pourForm.airContentPct) : null }), + }); + setPourForm({ station: '', pourDate: new Date().toISOString().slice(0, 10), mixDesign: 'Class A 40.7 MPa', volumeM3: '', slumpMm: '', airContentPct: '', payItemNo: '311(1)c' }); + setPours(null); + await api(`/mqc/projects/${project}/pours`).then(setPours); + } catch (e2) { setError((e2 as Error).message); } + finally { setSubmitting(false); } + } + + const pendingCount = tests?.filter((t) => t.result === 'pending').length ?? 0; + + if (error) return <>
{error}
; + if (!tests || !pours) return <>; + + return ( + <> + + +
+
+
Tests on file
+
{tests.length}
+
project {project}
+
0 ? 'ok' : 'info'}`} /> +
+
+
Pending certs
+
{pendingCount}
+
{pendingCount > 0 ? 'blocking billing' : 'none blocking'}
+
0 ? 'bad' : 'ok'}`} /> +
+
+
Pour logs
+
{pours.length}
+
project {project}
+
+
+
+
Project
+
+ +
+
 
+
+
+
+ + {/* DPWH requirements panel */} +
+
+ DPWH minimum testing requirements + +
+ {rules && rules.length > 0 + ? + + + {rules.map((r, i) => ( + + + + ))} + +
TestFrequencyQty
{r.test}{r.per}{r.qty}
+ :
No DPWH rules on file for this pay item.
+ } +
+ + {/* Tabs */} +
+ + +
+ + {tab === 'tests' && ( + <> +
+
Test results — {project}SRC · mqc.certs
+ + + + + + {tests.length === 0 + ? + : tests.map((t) => ( + + + + + + + + + + )) + } + +
Cert IDPay itemTestDateLab refResultBilling
No test results yet.
{t.certId ?? '—'}{t.payItemNo}{t.testType}{t.testDate ? t.testDate.slice(0, 10) : '—'}{t.labRef ?? '—'}{t.result.toUpperCase()}{t.billingRef ?? '—'}
+
+ +
+
Record test result — {project}
+
+ + + + + + + + +
+
+ + )} + + {tab === 'pours' && ( + <> +
+
Pour logs — {project}
+ + + + + + {pours.length === 0 + ? + : pours.map((p) => ( + + + + + + + + + + )) + } + +
DateStationPay itemMix designVolume (m³)Slump (mm)Air %
No pour logs yet.
{p.pourDate.slice(0, 10)}{p.station ?? '—'}{(p as unknown as { payItemNo?: string }).payItemNo ?? '—'}{p.mixDesign ?? '—'}{Number(p.volumeM3).toLocaleString()}{p.slumpMm ?? '—'}{p.airContentPct ?? '—'}
+
+ +
+
Log a pour — {project}
+
+ + + + + + + + +
+
+ + )} + + ); +} diff --git a/apps/web/src/pages/PmsScheduler.tsx b/apps/web/src/pages/PmsScheduler.tsx new file mode 100644 index 0000000..baa2b2c --- /dev/null +++ b/apps/web/src/pages/PmsScheduler.tsx @@ -0,0 +1,173 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { api } from '../api'; +import { Loader, SheetBar } from '../ui'; + +type PmsStatus = 'scheduled' | 'in-progress' | 'done'; +interface Pms { + id: string; assetQr: string; assetType: string; category: string; + quarter: string; dueOn: string; status: PmsStatus; tech: string | null; completedOn: string | null; +} +interface Cat { category: string; count: number } + +const YEAR = new Date().getFullYear(); +const QUARTERS = [1, 2, 3, 4]; +const Q_MONTHS: Record = { 1: [0, 1, 2], 2: [3, 4, 5], 3: [6, 7, 8], 4: [9, 10, 11] }; +const MONTH = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; +const COLS: { key: PmsStatus; label: string }[] = [ + { key: 'scheduled', label: 'Scheduled' }, + { key: 'in-progress', label: 'In progress' }, + { key: 'done', label: 'Done' }, +]; +const stTone = (s: PmsStatus) => (s === 'done' ? 'st-ok' : s === 'in-progress' ? 'st-warn' : 'st-info'); + +export default function PmsScheduler() { + const [q, setQ] = useState(Math.floor(new Date().getMonth() / 3) + 1); + const [view, setView] = useState<'list' | 'kanban' | 'calendar'>('list'); + const [items, setItems] = useState(null); + const [cats, setCats] = useState([]); + const [picked, setPicked] = useState>(new Set()); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + const quarter = `${YEAR}-Q${q}`; + + const load = useCallback(() => { + setItems(null); + api(`/it/pms?quarter=${quarter}`).then(setItems).catch((e: Error) => setError(e.message)); + }, [quarter]); + + useEffect(() => { load(); }, [load]); + useEffect(() => { api('/it/pms/categories').then(setCats).catch(() => {}); }, []); + + async function generate() { + setBusy(true); + try { + await api('/it/pms/generate', { method: 'POST', body: JSON.stringify({ quarter, categories: [...picked] }) }); + load(); + } catch (e) { setError(e instanceof Error ? e.message : 'failed'); } finally { setBusy(false); } + } + + async function advance(it: Pms) { + const next: PmsStatus = it.status === 'scheduled' ? 'in-progress' : it.status === 'in-progress' ? 'done' : 'scheduled'; + const tech = next === 'done' && !it.tech ? prompt('Tech who completed it?') ?? it.tech : it.tech; + await api(`/it/pms/${it.id}/status`, { method: 'POST', body: JSON.stringify({ status: next, tech: tech ?? undefined }) }); + load(); + } + + async function setTech(it: Pms, tech: string) { + await api(`/it/pms/${it.id}/status`, { method: 'POST', body: JSON.stringify({ status: it.status, tech }) }); + load(); + } + + const done = useMemo(() => (items ?? []).filter((i) => i.status === 'done').length, [items]); + + return ( + <> + +
+ ← IT control room + {quarter}: {items?.length ?? 0} scheduled · {done} done +
+ + {/* quarter tabs */} +
+ {QUARTERS.map((n) => ( + + ))} +
+ + {/* generate by category */} +
+
Generate {quarter} from Property assetsSRC · property.assets → it.pms
+
+ {cats.map((c) => { + const on = picked.has(c.category); + return ( + + ); + })} + +
+
+ + {/* view switcher */} +
+ {(['list', 'kanban', 'calendar'] as const).map((v) => ( + + ))} +
+ + {error &&
{error}
} + {!items ? : items.length === 0 ? ( +
No PMS for {quarter} yet — pick categories above and Generate.
+ ) : view === 'list' ? ( +
+ + + + {items.map((it) => ( + + + + + + + + ))} + +
AssetTypeCategoryDueTechStatusCompleted
{it.assetQr}{it.assetType}{it.category}{it.dueOn} e.target.value !== (it.tech ?? '') && void setTech(it, e.target.value)} />{it.status}{it.completedOn ?? }
+
+ ) : view === 'kanban' ? ( +
+ {COLS.map((col) => ( +
+
{col.label} {items.filter((i) => i.status === col.key).length}
+ {items.filter((i) => i.status === col.key).map((it) => ( +
void advance(it)} title="click to advance"> +
{it.assetQr}{it.category}
+
{it.assetType}
+
due {it.dueOn}{it.tech ?? 'unassigned'}
+
+ ))} +
+ ))} +
+ ) : ( +
+ {Q_MONTHS[q].map((m) => { + const first = new Date(YEAR, m, 1).getDay(); + const days = new Date(YEAR, m + 1, 0).getDate(); + const cells: (number | null)[] = [...Array(first).fill(null), ...Array.from({ length: days }, (_, i) => i + 1)]; + const onDay = (d: number) => items.filter((i) => new Date(i.dueOn).getMonth() === m && new Date(i.dueOn).getDate() === d); + return ( +
+
{MONTH[m]} {YEAR}
+
+ {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((d, i) =>
{d}
)} + {cells.map((d, i) => ( +
+ {d && {d}} + {d && onDay(d).map((it) => ( + + ))} +
+ ))} +
+
+ ); + })} +
+ )} + + ); +} diff --git a/apps/web/src/pages/Procurement.tsx b/apps/web/src/pages/Procurement.tsx new file mode 100644 index 0000000..d42d329 --- /dev/null +++ b/apps/web/src/pages/Procurement.tsx @@ -0,0 +1,65 @@ +import { useEffect, useState } from 'react'; +import { api } from '../api'; +import { Loader, SheetBar } from '../ui'; + +interface Intake { id: string; projectCode: string; weekOf: string; lines: number; approval: string; status: string } +interface Supplier { supplier: string; deliveries: number; avgLateDays: number } + +const tone = (s: string) => (s === 'ready-to-buy' ? 'st-ok' : s === 'returned' ? 'st-bad' : 'st-warn'); + +/** Procurement inbox — approved materials schedules arriving from Engineering. */ +export default function Procurement() { + const [intake, setIntake] = useState(null); + const [suppliers, setSuppliers] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + Promise.all([ + api('/procurement/materials-intake').then(setIntake), + api('/procurement/supplier-performance').then(setSuppliers), + ]).catch((e: Error) => setError(e.message)); + }, []); + + if (error) return <>
{error}
; + if (!intake) return <>; + + return ( + <> + +
+
+
Incoming materials schedulesSRC · procurement.materials-intake
+ {intake.length === 0 ?
No schedules yet — Engineering submits them via the materials schedule.
: ( + + + + {intake.map((r) => ( + + + + + + ))} + +
ProjectWeekLinesApprovalStatus
{r.projectCode}{r.weekOf}{r.lines}{r.approval}{r.status}
+ )} +
+ +
+
Supplier delivery performanceSRC · procurement.supplier-performance
+ + + + {suppliers.map((s) => ( + + + + + ))} + +
SupplierDeliveriesAvg late
{s.supplier}{s.deliveries} 1 ? 'st-bad' : 'st-ok'}`}>{s.avgLateDays}d
+
+
+ + ); +} diff --git a/apps/web/src/pages/Property.tsx b/apps/web/src/pages/Property.tsx new file mode 100644 index 0000000..6fb6428 --- /dev/null +++ b/apps/web/src/pages/Property.tsx @@ -0,0 +1,115 @@ +import { useEffect, useState } from 'react'; +import { api } from '../api'; +import { Loader, SheetBar, Stamp } from '../ui'; + +interface Asset { + id: string; + qr: string; + type: string; + status: 'in-use' | 'idle' | 'maintenance' | 'retired'; + location: string; + custodian: string; +} +interface Movement { id: string; kind: string; fromCustodian: string | null; toCustodian: string | null; location: string; status: string; by: string; note: string | null; createdAt: string; } + +const STATUS = ['in-use', 'idle', 'maintenance', 'retired'] as const; +const tone = (s: string) => (s === 'in-use' ? 'st-ok' : s === 'maintenance' ? 'st-warn' : s === 'retired' ? 'st-bad' : 'st-info'); + +export default function Property() { + const [assets, setAssets] = useState(null); + const [sel, setSel] = useState<{ asset: Asset; history: Movement[] } | null>(null); + const [error, setError] = useState(null); + const [form, setForm] = useState({ status: '', location: '', custodian: '', note: '' }); + const month = new Date().toISOString().slice(0, 7); + + const refresh = () => api('/property/assets').then(setAssets).catch((e: Error) => setError(e.message)); + useEffect(() => { void refresh(); }, []); + + async function open(qr: string) { + setError(null); + const data = await api<{ asset: Asset; history: Movement[] }>(`/property/assets/${qr}`); + setSel(data); + setForm({ status: data.asset.status, location: data.asset.location, custodian: data.asset.custodian, note: '' }); + } + + async function scan() { + if (!sel) return; + await api(`/property/assets/${sel.asset.qr}/scan`, { + method: 'POST', + body: JSON.stringify({ status: form.status, location: form.location, custodian: form.custodian, note: form.note || undefined }), + }); + await open(sel.asset.qr); + await refresh(); + } + + async function attest() { + if (!sel) return; + await api(`/property/assets/${sel.asset.qr}/attest`, { method: 'POST', body: JSON.stringify({ month, note: 'confirmed by custodian' }) }); + await open(sel.asset.qr); + } + + if (error) return <>
{error}
; + if (!assets) return <>; + + return ( + <> + +
+
+
AssetsSRC · property.assets
+ + + + {assets.map((a) => ( + void open(a.qr)} style={{ cursor: 'pointer' }}> + + + + ))} + +
QRTypeCustodianLocationStatus
{a.qr}{a.type}{a.custodian}{a.location}{a.status}
+
+ +
+
Scan / update{sel && {sel.asset.qr}}
+ {!sel ? ( +
Pick an asset to scan, re-custody, or attest.
+ ) : ( + <> + + + + +
+ + +
+
Movement history
+ + + {sel.history.map((m) => ( + + + + + + ))} + +
{m.kind}{m.toCustodian} · {m.location}{new Date(m.createdAt).toLocaleDateString()}
+ + )} +
+
+ + ); +} diff --git a/apps/web/src/pages/QuantityEntry.tsx b/apps/web/src/pages/QuantityEntry.tsx new file mode 100644 index 0000000..2568c01 --- /dev/null +++ b/apps/web/src/pages/QuantityEntry.tsx @@ -0,0 +1,100 @@ +import { FormEvent, useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import type { QuantityRollup } from '@ubi/types'; +import { api } from '../api'; +import { Chainage, Loader, SheetBar } from '../ui'; + +const peso = (n: number) => '₱' + (n / 1e6).toFixed(2) + 'M'; +function weekEnding(): string { + const d = new Date(); + const diff = 6 - d.getDay(); + d.setDate(d.getDate() + diff); + return d.toISOString().slice(0, 10); +} + +/** Dead-simple weekly accomplishment entry for Quantity Engineers onsite. + * One number per project per week → auto-rolls into the monthly report. */ +export default function QuantityEntry() { + const [rows, setRows] = useState(null); + const [error, setError] = useState(null); + const [sel, setSel] = useState(null); + const [weekOf, setWeekOf] = useState(weekEnding()); + const [amount, setAmount] = useState(''); + const [note, setNote] = useState(''); + const [saved, setSaved] = useState(false); + + const refresh = () => api('/quantity/rollup').then((r) => setRows(r.filter((x) => x.inhouseAmount > 0))).catch((e: Error) => setError(e.message)); + useEffect(() => { void refresh(); }, []); + + async function submit(e: FormEvent) { + e.preventDefault(); + if (!sel || !amount) return; + await api('/quantity/entry', { method: 'POST', body: JSON.stringify({ projectId: sel.projectId, weekOf, amount: Number(amount), note: note || undefined }) }); + setAmount(''); setNote(''); setSaved(true); + setTimeout(() => setSaved(false), 2500); + await refresh(); + setSel((s) => (s ? (rows?.find((r) => r.projectId === s.projectId) ?? s) : s)); + } + + if (error) return <>
{error}
; + if (!rows) return <>; + + return ( + <> + +
+ Monthly projection & materials → + Quantity head dashboard → +
+ +
+
+
Your projects — accomplishment to dateSRC · engineering.quantity
+ + + + {rows.map((r) => ( + setSel(r)} style={{ cursor: 'pointer', background: sel?.projectId === r.projectId ? 'rgba(194,65,12,0.06)' : undefined }}> + + + + + + + ))} + +
ProjectPEIn-houseProgressThis month
{r.code}
{r.name}
{r.pe}{peso(r.inhouseAmount)}{peso(r.thisMonth)}
+
+ +
+
Log this week{sel && {sel.code}}
+ {!sel ? ( +
Pick a project on the left, then enter what you accomplished this week.
+ ) : ( +
+ {saved &&
✓ Logged — running to-date updated.
} +
+ {sel.name} · to-date {(sel.pct * 100).toFixed(1)}% ({peso(sel.toDate)} of {peso(sel.inhouseAmount)}) +
+ + + + + {amount && Number(amount) > 0 && ( +
+ → projected to-date {(((sel.toDate + Number(amount)) / sel.inhouseAmount) * 100).toFixed(1)}% +
+ )} +
+ )} +
+
+ + ); +} diff --git a/apps/web/src/pages/QuantityHead.tsx b/apps/web/src/pages/QuantityHead.tsx new file mode 100644 index 0000000..a4777f6 --- /dev/null +++ b/apps/web/src/pages/QuantityHead.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import type { QuantityRollup } from '@ubi/types'; +import { Chainage, Loader, SheetBar } from '../ui'; +import { api } from '../api'; + +interface Head { + projects: number; totalInhouse: number; totalToDate: number; + overallPct: number; thisMonth: number; behind: number; rows: QuantityRollup[]; +} +const peso = (n: number) => '₱' + (n / 1e6).toFixed(1) + 'M'; + +/** Quantity section head dashboard — rolls every QE's weekly entries up to a + * section view; the same numbers flow to the CEO ManCom. */ +export default function QuantityHead() { + const [h, setH] = useState(null); + const [error, setError] = useState(null); + useEffect(() => { api('/quantity/head').then(setH).catch((e: Error) => setError(e.message)); }, []); + + if (error) return <>
{error}
; + if (!h) return <>; + + return ( + <> + +
Weekly entry →Projection & materials →
+ +
+
Projects
{h.projects}
with in-house amounts
+
Overall accomplishment
{(h.overallPct * 100).toFixed(1)}%
{peso(h.totalToDate)} of {peso(h.totalInhouse)}
+
This month
{peso(h.thisMonth)}
accomplishment logged
+
Behind plan
{h.behind}
below 85% to-date
+
+ +
+
Per-project accomplishmentSRC · engineering.quantity
+ + + + {h.rows.filter((r) => r.inhouseAmount > 0).map((r) => ( + + + + + + + + + + ))} + +
ProjectPEIn-houseTo-dateProgressThis monthLast entry
{r.code}
{r.name}
{r.pe}{peso(r.inhouseAmount)}{peso(r.toDate)}{peso(r.thisMonth)}{r.lastWeekOf ?? '—'}
+
+ + ); +} diff --git a/apps/web/src/pages/QuantityProjection.tsx b/apps/web/src/pages/QuantityProjection.tsx new file mode 100644 index 0000000..22a2dfb --- /dev/null +++ b/apps/web/src/pages/QuantityProjection.tsx @@ -0,0 +1,134 @@ +import { useEffect, useMemo, useState } from 'react'; +import { Link } from 'react-router-dom'; +import type { QuantityRollup } from '@ubi/types'; +import { api } from '../api'; +import { Loader, SheetBar } from '../ui'; + +/** Canonical DPWH material catalog (from the Monthly Projection sheet). */ +const MATERIALS_CATALOG: ReadonlyArray<{ name: string; unit: string }> = [ + { name: 'Cement Bulk', unit: 'bulk' }, + { name: 'Cement Tonner', unit: 'tonner bags' }, + { name: 'Cement 40kg', unit: '40kg' }, + { name: 'Ready Mix Concrete 4000 PSI', unit: 'cu.m' }, + { name: 'Steel sheet Pile (pc)', unit: 'pcs' }, + { name: 'Steel sheet Pile (lm)', unit: 'lm' }, + { name: 'Washed Sand', unit: 'cu.m' }, + { name: 'Coarse Sand', unit: 'cu.m' }, + { name: 'Gravel 3/4', unit: 'cu.m' }, + { name: 'Gravel G1', unit: 'cu.m' }, + { name: 'Item 200', unit: 'cu.m' }, + { name: 'Item 300', unit: 'cu.m' }, + { name: 'Boulders', unit: 'cu.m' }, + { name: 'Borrow Materials', unit: 'cu.m' }, + { name: 'Mountain Mix/Earthfill', unit: 'cu.m' }, + { name: 'Reinforcing Steel 12mmØ x 12 m', unit: 'pcs' }, + { name: 'Reinforcing Steel 16mmØ x 12 m', unit: 'pcs' }, + { name: 'Reinforcing Steel 20mmØ x 12 m', unit: 'pcs' }, + { name: 'Reinforcing Steel 25mmØ x 12 m', unit: 'pcs' }, + { name: 'Reinforcing Steel Bar G40 (kg)', unit: 'kg' }, +]; + +interface Projection { projectId: string; month: string; projectedAmount: number } +interface Material { projectId: string; month: string; material: string; unit: string; qty: number } + +function nextMonths(n: number): string[] { + const out: string[] = []; + const d = new Date(); + for (let i = 1; i <= n; i++) { + const m = new Date(d.getFullYear(), d.getMonth() + i, 1); + out.push(`${m.getFullYear()}-${String(m.getMonth() + 1).padStart(2, '0')}`); + } + return out; +} +const label = (m: string) => new Date(m + '-01').toLocaleString('en', { month: 'short', year: '2-digit' }); + +/** On-platform replacement for the Google-Drive "Monthly Projection of + * Accomplishment & Material Requirements" sheet — one platform for all edges. */ +export default function QuantityProjection() { + const [projects, setProjects] = useState([]); + const [pid, setPid] = useState(''); + const [proj, setProj] = useState>({}); + const [mats, setMats] = useState>({}); // key `${month}|${material}` + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + const months = useMemo(() => nextMonths(6), []); + + useEffect(() => { + api('/quantity/rollup') + .then((r) => { const f = r.filter((x) => x.inhouseAmount > 0); setProjects(f); if (f[0]) setPid(f[0].projectId); }) + .catch((e: Error) => setError(e.message)) + .finally(() => setLoading(false)); + }, []); + + useEffect(() => { + if (!pid) return; + Promise.all([ + api(`/quantity/projections?projectId=${pid}`), + api(`/quantity/materials?projectId=${pid}`), + ]).then(([ps, ms]) => { + setProj(Object.fromEntries(ps.map((p) => [p.month, p.projectedAmount]))); + setMats(Object.fromEntries(ms.map((m) => [`${m.month}|${m.material}`, m.qty]))); + }).catch((e: Error) => setError(e.message)); + }, [pid]); + + async function saveProjection(month: string, val: number) { + await api('/quantity/projection', { method: 'POST', body: JSON.stringify({ projectId: pid, month, projectedAmount: val }) }); + setProj((p) => ({ ...p, [month]: val })); + } + async function saveMaterial(month: string, material: string, unit: string, val: number) { + await api('/quantity/material', { method: 'POST', body: JSON.stringify({ projectId: pid, month, material, unit, qty: val }) }); + setMats((m) => ({ ...m, [`${month}|${material}`]: val })); + } + + if (error) return <>
{error}
; + if (loading) return <>; + + return ( + <> + +
+ ← Weekly entry + +
+ +
+
Projection gridSRC · engineering.material-reqs · type to save
+ + + + + {months.map((m) => )} + + + + + + {months.map((m) => ( + + ))} + + {MATERIALS_CATALOG.map((mat) => ( + + + {months.map((m) => { + const k = `${m}|${mat.name}`; + return ( + + ); + })} + + ))} + +
ParticularUnit{label(m)}
PROJECTED MONTHLY ACCOMP. + { const v = Number(e.target.value || 0); if (v !== (proj[m] ?? 0)) void saveProjection(m, v); }} /> +
{mat.name}{mat.unit} + { const v = Number(e.target.value || 0); if (v !== (mats[k] ?? 0)) void saveMaterial(m, mat.name, mat.unit, v); }} /> +
+
+ + ); +} diff --git a/apps/web/src/pages/Records.tsx b/apps/web/src/pages/Records.tsx new file mode 100644 index 0000000..4f50630 --- /dev/null +++ b/apps/web/src/pages/Records.tsx @@ -0,0 +1,78 @@ +import { FormEvent, useEffect, useState } from 'react'; +import { api } from '../api'; +import { Loader, SheetBar } from '../ui'; + +interface Expiry { title: string; kind: string; expiresOn: string; daysLeft: number } +interface IndexRow { title: string; cabinet: string; drawer: string; sensitive: boolean; access: string } + +export default function Records() { + const [expiry, setExpiry] = useState(null); + const [index, setIndex] = useState(null); + const [error, setError] = useState(null); + const [vd, setVd] = useState({ plate: '', kind: 'registration', refNo: '', expiresOn: '' }); + + const refresh = () => + Promise.all([ + api('/records/expiry').then(setExpiry), + api('/records/physical-index').then(setIndex), + ]).catch((e: Error) => setError(e.message)); + useEffect(() => { void refresh(); }, []); + + async function addVehicle(e: FormEvent) { + e.preventDefault(); + await api('/records/vehicle-docs', { method: 'POST', body: JSON.stringify(vd) }); + setVd({ plate: '', kind: 'registration', refNo: '', expiresOn: '' }); + await refresh(); + } + + if (error) return <>
{error}
; + if (!expiry || !index) return <>; + + return ( + <> + + +
+
+
Expiring ≤60 daysSRC · records.expiry
+ + + + {expiry.map((r, i) => ( + + + + + ))} + +
DocumentKindExpiresDays
{r.title}{r.kind}{r.expiresOn}{r.daysLeft}d
+
+ setVd({ ...vd, plate: e.target.value })} required /> + + setVd({ ...vd, refNo: e.target.value })} required /> + setVd({ ...vd, expiresOn: e.target.value })} required /> + +
+
+ +
+
Physical location indexSRC · records.physical-index · access-gated
+ + + + {index.map((r, i) => ( + + + + + ))} + +
DocumentCabinetDrawerAccess
{r.sensitive && '🔒 '}{r.title}{r.cabinet}{r.drawer}{r.access}
+
Restricted rows are hidden unless you hold the required claim (e.g. land titles need role:legal).
+
+
+ + ); +} diff --git a/apps/web/src/pages/Survey.tsx b/apps/web/src/pages/Survey.tsx new file mode 100644 index 0000000..1e32022 --- /dev/null +++ b/apps/web/src/pages/Survey.tsx @@ -0,0 +1,143 @@ +import { FormEvent, useEffect, useState } from 'react'; +import { api } from '../api'; +import { TrendChart } from '../charts'; +import { Loader, SheetBar } from '../ui'; + +interface Measurement { + id: string; + projectId: string; + period: string; + stationStart: string; + stationEnd: string; + payItemNo: string; + volumeM3: number; + type: string; +} + +import type { TrendPoint } from '@ubi/types'; + +const PROJECTS = ['PKG-02', 'PKG-05', 'PKG-11', 'PKG-03', 'PKG-07', 'PKG-09']; +const TYPES = ['cross-section', 'original-ground', 'stakeout', 'as-built']; + +export default function Survey() { + const [trend, setTrend] = useState(null); + const [measurements, setMeasurements] = useState(null); + const [project, setProject] = useState('PKG-02'); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + const [form, setForm] = useState({ + stationStart: '', stationEnd: '', payItemNo: '311(1)c', + volumeM3: '', type: 'cross-section', period: new Date().toISOString().slice(0, 7), + notes: '', + }); + + const refresh = () => + Promise.all([ + api('/survey/volumes').then(setTrend), + api(`/survey/projects/${project}`).then(setMeasurements), + ]).catch((e: Error) => setError(e.message)); + + useEffect(() => { void refresh(); }, [project]); + + async function submit(e: FormEvent) { + e.preventDefault(); + setSubmitting(true); + try { + await api(`/survey/projects/${project}/measurements`, { + method: 'POST', + body: JSON.stringify({ ...form, volumeM3: Number(form.volumeM3) }), + }); + setForm({ stationStart: '', stationEnd: '', payItemNo: '311(1)c', volumeM3: '', type: 'cross-section', period: new Date().toISOString().slice(0, 7), notes: '' }); + await refresh(); + } catch (e2) { setError((e2 as Error).message); } + finally { setSubmitting(false); } + } + + if (error) return <>
{error}
; + if (!trend || !measurements) return <>; + + return ( + <> + + +
+
+
Monthly volume trend (m³)SRC · survey.volumes
+ {trend.length === 0 + ?
No measurements recorded yet.
+ : + } +
+ +
+
+ Measurements — project + +
+ + + + + + {measurements.length === 0 + ? + : measurements.map((m) => ( + + + + + + + + )) + } + +
PeriodStationPay itemTypeVolume (m³)
No measurements for this project yet.
{m.period}{m.stationStart} → {m.stationEnd}{m.payItemNo}{m.type}{Number(m.volumeM3).toLocaleString()}
+
+
+ +
+
Submit measurement — {project}
+
+ + + + + + + + +
+
+ + ); +} diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css new file mode 100644 index 0000000..a590ad4 --- /dev/null +++ b/apps/web/src/styles.css @@ -0,0 +1,475 @@ +/* ═══════════════════════════════════════════════════════════════════ + UBI Suite — "THE DRAWING COMES ALIVE" + An engineering-drawing design language: drafting-grid paper, sheet + cards with crop marks, title-block headers, rubber-stamp statuses, + chainage rulers, hazard-stripe criticals, topo-contour chrome. + Type: Space Grotesk (display/body) + IBM Plex Mono (labels/codes). + ═══════════════════════════════════════════════════════════════════ */ +:root { + --ink: #102536; + --ink2: #23476b; + --orange: #c2410c; + --orange-d: #9a3409; + --teal: #0e7490; + --blue: #1d4ed8; + --ok: #15803d; + --warn: #b45309; + --bad: #b91c1c; + --muted: #5b6b7a; + --paper: #eff2ee; + --sheet: #fdfdfb; + --line: #d7dee3; + --grid: rgba(35, 71, 107, 0.07); + --mono: 'IBM Plex Mono', ui-monospace, Menlo, monospace; + --disp: 'Space Grotesk Variable', 'Space Grotesk', 'Segoe UI', system-ui, sans-serif; + --shadow: 4px 4px 0 rgba(16, 37, 54, 0.16); +} +* { box-sizing: border-box; } +html, body, #root { margin: 0; height: 100%; } +body { + background: + repeating-linear-gradient(0deg, transparent, transparent 23px, var(--grid) 23px, var(--grid) 24px), + repeating-linear-gradient(90deg, transparent, transparent 23px, var(--grid) 23px, var(--grid) 24px), + var(--paper); + color: var(--ink); + font-family: var(--disp); + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} +button { font: inherit; cursor: pointer; } +input, select { font: inherit; } +a { color: var(--blue); text-decoration: none; } +code { font-family: var(--mono); font-size: 0.9em; letter-spacing: -0.2px; } +::selection { background: var(--orange); color: #fff; } + +/* ── shell ─────────────────────────────────────────────────────── */ +.shell { display: flex; min-height: 100vh; } +.side { + flex: 0 0 222px; color: #cfdae4; display: flex; flex-direction: column; + padding: 16px 0 12px; position: sticky; top: 0; height: 100vh; overflow-y: auto; + background: + radial-gradient(ellipse 160% 30% at 30% 8%, rgba(194, 65, 12, 0.12), transparent 60%), + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='420' viewBox='0 0 240 420'%3E%3Cg fill='none' stroke='%23ffffff' stroke-opacity='0.05' stroke-width='1.2'%3E%3Cpath d='M-20,80 C60,40 140,120 260,70'/%3E%3Cpath d='M-20,110 C70,70 150,150 260,100'/%3E%3Cpath d='M-20,140 C80,100 160,180 260,130'/%3E%3Cpath d='M-20,300 C60,260 150,340 260,290'/%3E%3Cpath d='M-20,330 C70,290 160,370 260,320'/%3E%3Cpath d='M-20,360 C80,320 170,400 260,350'/%3E%3C/g%3E%3C/svg%3E"), + linear-gradient(180deg, #0d1f2e, #102536 40%, #0d1f2e); + border-right: 2px solid var(--ink); +} +.sbrand { display: flex; align-items: center; gap: 10px; padding: 2px 16px 14px; border-bottom: 1px dashed #2c4d6c; } +.slogo { + width: 34px; height: 34px; background: var(--orange); color: #fff; flex: none; + display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 17px; + clip-path: polygon(0 0, 100% 0, 100% 72%, 72% 100%, 0 100%); + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.35); +} +.sbrand b { display: block; color: #fff; font-size: 15px; line-height: 1.1; letter-spacing: 0.3px; } +.sbrand span { font-family: var(--mono); font-size: 8.5px; color: #8fa6b8; letter-spacing: 2.2px; text-transform: uppercase; } +.sentity { + margin: 12px 14px 2px; background: rgba(255, 255, 255, 0.05); border: 1px solid #2c4d6c; + padding: 7px 11px; font-family: var(--mono); font-size: 10px; letter-spacing: 1px; + color: #dbe7f1; display: flex; justify-content: space-between; text-transform: uppercase; +} +.sentity b { color: #ffb38a; } +.sgrp { + padding: 15px 16px 5px; font-family: var(--mono); font-size: 8.5px; + letter-spacing: 2.4px; color: #6f8aa0; text-transform: uppercase; + display: flex; align-items: center; gap: 8px; +} +.sgrp::after { content: ''; flex: 1; border-top: 1px dashed #2c4d6c; } +.sitem { + position: relative; display: block; padding: 7px 16px 7px 26px; + font-size: 13.5px; font-weight: 500; color: #cfdae4; +} +.sitem::before { + content: ''; position: absolute; left: 14px; top: 50%; width: 5px; height: 1.5px; + background: #4a6a88; transform: translateY(-50%); +} +.sitem:hover { background: rgba(255, 255, 255, 0.06); color: #fff; } +.sitem.on { background: rgba(194, 65, 12, 0.16); color: #fff; font-weight: 700; } +.sitem.on::before { + width: 0; height: 0; background: none; border-left: 7px solid var(--orange); + border-top: 5px solid transparent; border-bottom: 5px solid transparent; left: 12px; +} +.sitem.dim { color: #8fa6b8; font-size: 12.5px; padding-top: 5px; padding-bottom: 5px; } +.suser { margin-top: auto; border-top: 1px dashed #2c4d6c; padding: 12px 16px; display: flex; gap: 9px; align-items: center; } +.suser .av { + width: 30px; height: 30px; flex: none; background: #1c3954; color: #ffb38a; + border: 1px solid #2c4d6c; font-family: var(--mono); font-size: 11px; font-weight: 700; + display: flex; align-items: center; justify-content: center; +} +.suser b { display: block; color: #fff; font-size: 12px; line-height: 1.2; } +.suser span { font-family: var(--mono); font-size: 8px; color: #8fa6b8; letter-spacing: 0.3px; } +.suser button { + margin-left: auto; background: none; border: 1px solid #2c4d6c; color: #8fa6b8; + font-family: var(--mono); font-size: 9px; letter-spacing: 1px; padding: 4px 8px; text-transform: uppercase; +} +.suser button:hover { color: #fff; border-color: var(--orange); } + +.main { flex: 1; min-width: 0; padding: 18px 26px 40px; } + +/* ── sheet title block ─────────────────────────────────────────── */ +.sheetbar { + display: flex; border: 2px solid var(--ink); background: var(--sheet); + margin-bottom: 16px; box-shadow: var(--shadow); +} +.sheetbar .cell { padding: 7px 14px 8px; border-right: 1.5px solid var(--ink); min-width: 0; } +.sheetbar .cell:last-child { border-right: 0; } +.sheetbar .cell span { + display: block; font-family: var(--mono); font-size: 7.5px; letter-spacing: 2px; + color: var(--muted); text-transform: uppercase; +} +.sheetbar .cell b { font-size: 15px; letter-spacing: 0.2px; white-space: nowrap; } +.sheetbar .code b, .sheetbar .rev b, .sheetbar .date b { font-family: var(--mono); font-size: 13px; } +.sheetbar .code { background: var(--ink); } +.sheetbar .code span { color: #8fa6b8; } +.sheetbar .code b { color: #ffb38a; } +.sheetbar .name { flex: 1; } +.sheetbar .name b { display: block; overflow: hidden; text-overflow: ellipsis; } +.sheetbar .note { max-width: 320px; } +.sheetbar .note b { font-size: 11px; font-weight: 500; color: var(--muted); white-space: normal; line-height: 1.3; display: block; } + +.top { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 14px; } +.top h1 { margin: 0; font-size: 22px; font-weight: 700; letter-spacing: -0.3px; } +.top .right { margin-left: auto; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } +.asof { font-family: var(--mono); font-size: 10px; letter-spacing: 0.5px; color: var(--muted); display: flex; align-items: center; gap: 6px; text-transform: uppercase; } +.asof i { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px rgba(21, 128, 61, 0.18); } + +/* ── sheets (cards) with crop marks ────────────────────────────── */ +.card { + position: relative; background: var(--sheet); border: 1.5px solid var(--ink); + padding: 15px 17px; box-shadow: var(--shadow); +} +.card::before, .card::after { + content: ''; position: absolute; width: 9px; height: 9px; pointer-events: none; +} +.card::before { top: 3px; left: 3px; border-top: 1.5px solid var(--ink2); border-left: 1.5px solid var(--ink2); } +.card::after { bottom: 3px; right: 3px; border-bottom: 1.5px solid var(--ink2); border-right: 1.5px solid var(--ink2); } +.ph { display: flex; align-items: center; gap: 10px; margin-bottom: 9px; } +.ph b { font-size: 13.5px; font-weight: 700; letter-spacing: 0.1px; } +.src { + margin-left: auto; font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.6px; + color: var(--blue); background: #eef3ff; border: 1px solid #b9cdf5; padding: 2px 8px; white-space: nowrap; +} + +/* ── controls ──────────────────────────────────────────────────── */ +.btn { + border: 2px solid var(--ink); background: var(--sheet); color: var(--ink); + padding: 7px 16px; font-weight: 700; font-size: 12.5px; letter-spacing: 0.3px; + box-shadow: 3px 3px 0 rgba(16, 37, 54, 0.22); transition: transform 0.06s, box-shadow 0.06s; +} +.btn:hover { transform: translate(-1px, -1px); box-shadow: 4px 4px 0 rgba(16, 37, 54, 0.26); } +.btn:active { transform: translate(1px, 1px); box-shadow: 1px 1px 0 rgba(16, 37, 54, 0.3); } +.btn.pri { background: var(--orange); color: #fff; } +.btn.pri:hover { background: var(--orange-d); } +.btn.navy { background: var(--ink); color: #fff; } +.btn.ok { background: var(--ok); color: #fff; } +.btn.sm { padding: 4px 11px; font-size: 11px; box-shadow: 2px 2px 0 rgba(16, 37, 54, 0.22); } +.btn:disabled { opacity: 0.45; cursor: default; transform: none; } + +.field { display: block; margin-bottom: 13px; } +.field span { + display: block; font-family: var(--mono); font-size: 9px; font-weight: 600; + letter-spacing: 1.8px; color: var(--muted); margin-bottom: 4px; text-transform: uppercase; +} +.field input, .field select { + width: 100%; border: 1.6px solid var(--ink); background: #fff; padding: 9px 12px; font-size: 14px; +} +.field input:focus { outline: 2px solid var(--orange); outline-offset: 1px; } + +.toggle { + display: flex; align-items: center; gap: 8px; font-family: var(--mono); font-size: 10px; + font-weight: 600; letter-spacing: 0.6px; text-transform: uppercase; background: var(--sheet); + border: 1.5px solid var(--ink); padding: 5px 12px 5px 6px; cursor: pointer; user-select: none; + box-shadow: 2px 2px 0 rgba(16, 37, 54, 0.18); +} +.tg { width: 32px; height: 17px; background: #c8d2da; border: 1.5px solid var(--ink); position: relative; flex: none; transition: background 0.15s; } +.tg::after { content: ''; position: absolute; top: 1px; left: 1px; width: 12px; height: 12px; background: var(--ink); transition: transform 0.15s; } +.toggle.on .tg { background: #ffd9c4; } +.toggle.on .tg::after { transform: translateX(15px); background: var(--orange); } + +/* ── stamps ────────────────────────────────────────────────────── */ +.stamp { + display: inline-block; font-family: var(--mono); font-size: 9.5px; font-weight: 700; + letter-spacing: 2px; text-transform: uppercase; padding: 3px 9px 2px; + border: 2px solid currentColor; box-shadow: inset 0 0 0 1.5px var(--sheet), inset 0 0 0 2.5px currentColor; + transform: rotate(-3deg); opacity: 0.92; white-space: nowrap; + mask-image: radial-gradient(circle at 30% 60%, black 96%, transparent 100%); +} +.stamp-ok { color: var(--ok); } +.stamp-warn { color: var(--warn); } +.stamp-bad { color: var(--bad); } +.stamp-info { color: var(--blue); } + +/* status pills (inline, unrotated) */ +.st { font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: 0.8px; padding: 2px 8px; white-space: nowrap; text-transform: uppercase; border: 1.3px solid; } +.st-ok { background: #e9f6ee; color: var(--ok); border-color: #9fd4b4; } +.st-warn { background: #fdf3e7; color: var(--warn); border-color: #ecc796; } +.st-bad { background: #fdecec; color: var(--bad); border-color: #efa9a9; } +.st-info { background: #eef3ff; color: var(--blue); border-color: #b9cdf5; } +.neg { color: var(--bad); font-weight: 700; } +.pos { color: var(--ok); font-weight: 700; } + +/* ── tables: ledger ruling ─────────────────────────────────────── */ +.tbl { width: 100%; border-collapse: collapse; font-size: 12.5px; } +.tbl th { + font-family: var(--mono); font-size: 8.5px; letter-spacing: 1.6px; text-transform: uppercase; + color: var(--muted); text-align: left; padding: 4px 8px; font-weight: 600; + border-bottom: 2.5px double var(--ink); +} +.tbl td { padding: 7px 8px; border-bottom: 1px solid var(--line); vertical-align: middle; } +.tbl tr:last-child td { border-bottom: 0; } +.tbl tbody tr:hover { background: rgba(35, 71, 107, 0.045); } + +/* ── KPIs: title-block cells ───────────────────────────────────── */ +.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(158px, 1fr)); margin-bottom: 16px; border: 2px solid var(--ink); background: var(--sheet); box-shadow: var(--shadow); } +.kpi { padding: 12px 16px 11px; position: relative; box-shadow: -1.5px -1.5px 0 0 var(--ink); } +.kpi .lab { font-family: var(--mono); font-size: 8px; letter-spacing: 2px; color: var(--muted); text-transform: uppercase; } +.kpi .val { font-size: 30px; font-weight: 700; letter-spacing: -1px; margin: 2px 0 1px; display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; font-variant-numeric: tabular-nums; } +.kpi .sub { font-size: 11px; color: var(--muted); } +.d-up { font-family: var(--mono); font-size: 11px; font-weight: 700; color: var(--ok); } +.mchip { font-family: var(--mono); font-size: 8.5px; font-weight: 700; letter-spacing: 0.4px; padding: 1px 7px; border: 1.3px solid; } +.mc-bad { background: #fdecec; color: var(--bad); border-color: #efa9a9; } +.mc-proj { background: #fff5f0; color: var(--orange); border-style: dashed; border-color: #e8a886; } + +.chainage { display: block; width: 100%; max-width: 230px; height: 30px; } + +/* ── layout grids ──────────────────────────────────────────────── */ +.grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 14px; margin-bottom: 14px; } +.mancom { display: grid; grid-template-columns: 1fr 296px; gap: 16px; align-items: start; } +@media (max-width: 1100px) { .mancom { grid-template-columns: 1fr; } } +.rail { display: flex; flex-direction: column; gap: 11px; } +.railh { + font-family: var(--mono); font-size: 9.5px; letter-spacing: 2.2px; color: var(--ink2); + text-transform: uppercase; display: flex; align-items: center; gap: 8px; +} +.railh::before { content: '◢'; color: var(--orange); font-size: 8px; } +.railh a { margin-left: auto; font-size: 11px; letter-spacing: 0; } + +/* ── insights: field memos w/ hazard stripes ───────────────────── */ +.ins { position: relative; background: var(--sheet); border: 1.5px solid var(--ink); padding: 11px 13px 11px 19px; box-shadow: 3px 3px 0 rgba(16, 37, 54, 0.14); } +.ins::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 7px; } +.ins.critical::before { background: repeating-linear-gradient(135deg, var(--bad) 0 7px, var(--ink) 7px 14px); } +.ins.warning::before { background: repeating-linear-gradient(135deg, var(--warn) 0 7px, #f3e0c2 7px 14px); } +.ins.positive::before { background: var(--ok); } +.ins.info::before { background: var(--blue); } +.ins .sev { font-family: var(--mono); font-size: 8px; letter-spacing: 2px; font-weight: 700; } +.ins.critical .sev { color: var(--bad); } +.ins.warning .sev { color: var(--warn); } +.ins.positive .sev { color: var(--ok); } +.ins.info .sev { color: var(--blue); } +.ins p { margin: 4px 0 7px; font-size: 12px; line-height: 1.45; } +.evi { display: flex; flex-wrap: wrap; gap: 5px; } +.evi i { + font-style: normal; font-family: var(--mono); font-size: 8.5px; color: var(--teal); + background: #e9f5f6; border: 1px solid #a3d2da; padding: 1px 6px; +} +.gen { font-family: var(--mono); font-size: 8.5px; color: var(--muted); margin-top: 8px; letter-spacing: 0.4px; } + +/* ── legend / charts ───────────────────────────────────────────── */ +.lgnd { display: flex; gap: 14px; font-family: var(--mono); font-size: 9px; letter-spacing: 0.4px; color: var(--muted); margin-top: 7px; flex-wrap: wrap; text-transform: uppercase; } +.lgnd span { display: inline-flex; align-items: center; gap: 6px; } +.lk { width: 16px; height: 0; border-top: 2.6px solid; display: inline-block; } + +/* ── login: a drafting sheet ───────────────────────────────────── */ +.loginwrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; } +.login { width: 430px; position: relative; } +.login .sheet { + position: relative; background: var(--sheet); border: 2px solid var(--ink); padding: 26px 28px 0; + box-shadow: 7px 7px 0 rgba(16, 37, 54, 0.18); + background-image: + repeating-linear-gradient(0deg, transparent, transparent 23px, var(--grid) 23px, var(--grid) 24px), + repeating-linear-gradient(90deg, transparent, transparent 23px, var(--grid) 23px, var(--grid) 24px); +} +.login .fieldcopy { + position: absolute; top: 14px; right: -14px; transform: rotate(6deg); z-index: 2; + font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: 2px; color: var(--orange); + border: 2px solid var(--orange); padding: 5px 10px; background: var(--sheet); + box-shadow: inset 0 0 0 1.5px var(--sheet), inset 0 0 0 2.5px var(--orange); text-align: center; line-height: 1.5; +} +.login .brand { display: flex; align-items: center; gap: 13px; margin-bottom: 20px; } +.login .slogo { width: 48px; height: 48px; font-size: 23px; } +.login h1 { margin: 0; font-size: 20px; font-weight: 700; letter-spacing: -0.3px; } +.login .sub { font-family: var(--mono); font-size: 9px; color: var(--muted); letter-spacing: 2.4px; text-transform: uppercase; } +.login form { padding-bottom: 22px; } +.login .titleblock { + display: flex; border-top: 2px solid var(--ink); margin: 0 -28px; + font-family: var(--mono); font-size: 8px; letter-spacing: 1px; text-transform: uppercase; color: var(--muted); +} +.login .titleblock div { padding: 6px 12px 7px; border-right: 1.5px solid var(--ink); flex: 1; } +.login .titleblock div:last-child { border-right: 0; } +.login .titleblock b { display: block; font-size: 10.5px; color: var(--ink); letter-spacing: 0.5px; } +.err { background: #fdecec; border: 1.5px solid var(--bad); color: var(--bad); padding: 9px 12px; font-size: 12.5px; margin-bottom: 12px; } +.hint { border: 1.5px dashed var(--teal); background: #f0f7f8; color: #0c5e75; padding: 10px 13px; font-size: 12px; margin-top: 16px; } +.demos { margin-top: 16px; border-top: 1px dashed var(--line); padding-top: 12px; } +.demos-h { font-family: var(--mono); font-size: 9px; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); margin-bottom: 8px; } +.demos-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; } +.demo-chip { border: 1.5px solid var(--ink); background: var(--sheet); font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase; padding: 5px 11px; box-shadow: 2px 2px 0 rgba(16,37,54,0.16); } +.demo-chip:hover { background: var(--orange); color: #fff; border-color: var(--orange); transform: translate(-1px,-1px); } +.demo-select { border: 1.5px solid var(--ink); background: var(--sheet); font-family: var(--mono); font-size: 11px; padding: 6px 10px; width: 100%; } + +/* ── chat ──────────────────────────────────────────────────────── */ +.chat { display: flex; flex-direction: column; gap: 11px; max-width: 740px; } +.bub { padding: 10px 14px; font-size: 13px; line-height: 1.5; max-width: 86%; white-space: pre-wrap; border: 1.5px solid var(--ink); } +.bub.me { align-self: flex-end; background: var(--ink); color: #fff; box-shadow: 3px 3px 0 rgba(194, 65, 12, 0.45); } +.bub.ai { align-self: flex-start; background: var(--sheet); box-shadow: 3px 3px 0 rgba(16, 37, 54, 0.16); } +.askrow { display: flex; gap: 10px; max-width: 740px; } +.askrow input { flex: 1; border: 2px solid var(--ink); background: var(--sheet); padding: 10px 16px; box-shadow: 3px 3px 0 rgba(16, 37, 54, 0.14); } +.askrow input:focus { outline: 2px solid var(--orange); outline-offset: 1px; } + +/* ── fleet live map ────────────────────────────────────────────── */ +.fleetgrid { display: grid; grid-template-columns: 1fr 340px; gap: 14px; align-items: start; } +@media (max-width: 1000px) { .fleetgrid { grid-template-columns: 1fr; } } +.fleetgrid .leaflet-container { border: 0; font-family: var(--disp); } + +/* ── department dashboard: monitoring + CCTV ───────────────────── */ +.deptgrid { display: grid; grid-template-columns: 1fr 320px; gap: 14px; align-items: start; } +@media (max-width: 1000px) { .deptgrid { grid-template-columns: 1fr; } } +.kpi-bar { height: 4px; margin-top: 9px; background: var(--line); } +.kpi-bar.tone-ok { background: var(--ok); } +.kpi-bar.tone-warn { background: var(--warn); } +.kpi-bar.tone-bad { background: repeating-linear-gradient(135deg, var(--bad) 0 6px, var(--ink) 6px 12px); } +.kpi-bar.tone-info { background: var(--teal); } +.kpi .val .mc-proj { display: none; } + +.cctv-card { display: flex; flex-direction: column; } +.cctv { border: 2px solid var(--ink); background: #07121b; color: #c7d6e2; font-family: var(--mono); position: relative; overflow: hidden; } +.cctv.live { border-color: var(--ok); } +.cctv-top, .cctv-bot { display: flex; justify-content: space-between; align-items: center; padding: 4px 8px; font-size: 9px; letter-spacing: 1px; background: rgba(0,0,0,0.5); position: relative; z-index: 2; } +.cctv-rec { color: #ff5a5a; font-weight: 700; } +.cctv.live .cctv-rec { color: #4ade80; animation: recblink 1.4s steps(1) infinite; } +@keyframes recblink { 50% { opacity: 0.35; } } +.cctv-ts { color: #8fa6b8; } +.cctv-noise { height: 150px; position: relative; background: + repeating-linear-gradient(0deg, rgba(255,255,255,0.035) 0 1px, transparent 1px 3px), + radial-gradient(ellipse at 50% 40%, #16313f, #07121b 70%); } +.cctv-feed { width: 100%; height: 150px; object-fit: cover; display: block; } +.cctv-cross { position: absolute; inset: 0; } +.cctv-cross::before, .cctv-cross::after { content: ''; position: absolute; background: rgba(143,166,184,0.25); } +.cctv-cross::before { left: 50%; top: 0; bottom: 0; width: 1px; } +.cctv-cross::after { top: 50%; left: 0; right: 0; height: 1px; } +.cctv-offline { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 11px; letter-spacing: 3px; color: #5b7488; } +.cctv-bot .cctv-dot { width: 7px; height: 7px; border-radius: 50%; background: #ff5a5a; } +.cctv.live .cctv-bot .cctv-dot { background: #4ade80; } + +.muted { color: var(--muted); } +.spread { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.spread .right { margin-left: auto; } +.mb { margin-bottom: 14px; } + +/* ── floating AI assistant (icon-only, bottom-right) ───────────── */ +.fab { + position: fixed; right: 22px; bottom: 22px; z-index: 50; + width: 56px; height: 56px; border-radius: 50%; + background: var(--orange); color: #fff; border: 2px solid var(--ink); + display: flex; align-items: center; justify-content: center; + box-shadow: 3px 3px 0 rgba(16,37,54,0.35); transition: transform .08s, box-shadow .08s; +} +.fab:hover { transform: translate(-1px,-1px); box-shadow: 5px 5px 0 rgba(16,37,54,0.4); } +.fab:active { transform: translate(1px,1px); box-shadow: 1px 1px 0 rgba(16,37,54,0.4); } +.fab.on { background: var(--ink); } +.assistant-panel { + position: fixed; right: 22px; bottom: 88px; z-index: 50; + width: 370px; max-width: calc(100vw - 32px); height: 480px; max-height: calc(100vh - 120px); + display: flex; flex-direction: column; + background: var(--sheet); border: 2px solid var(--ink); box-shadow: 6px 6px 0 rgba(16,37,54,0.22); +} +.ap-head { + display: flex; align-items: center; gap: 8px; padding: 9px 12px; background: var(--ink); color: #fff; + font-family: var(--mono); font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase; +} +.ap-dot { width: 8px; height: 8px; border-radius: 50%; background: #4ade80; box-shadow: 0 0 0 3px rgba(74,222,128,.2); } +.ap-expand { margin-left: auto; color: #9fb4c7; text-decoration: none; font-size: 14px; } +.ap-expand:hover { color: #fff; } +.ap-x { background: none; border: 0; color: #9fb4c7; font-size: 12px; } +.ap-x:hover { color: #fff; } +.ap-body { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 9px; background: + repeating-linear-gradient(0deg, transparent, transparent 23px, var(--grid) 23px, var(--grid) 24px), + repeating-linear-gradient(90deg, transparent, transparent 23px, var(--grid) 23px, var(--grid) 24px), var(--sheet); } +.ap-body .bub { max-width: 92%; font-size: 12px; } +.ap-empty { color: var(--muted); font-size: 12px; } +.ap-sugg { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; } +.ap-sugg button { text-align: left; border: 1px dashed #ccdaf8; background: #f6f9ff; color: var(--blue); font-size: 11px; padding: 6px 10px; } +.ap-sugg button:hover { background: #eef3ff; } +.ap-input { display: flex; gap: 7px; padding: 9px; border-top: 1.5px solid var(--ink); background: var(--sheet); } +.ap-input input { flex: 1; border: 1.5px solid var(--ink); background: #fff; padding: 8px 11px; font-size: 12.5px; } +.ap-input input:focus { outline: 2px solid var(--orange); outline-offset: -1px; } +@media print { .fab, .assistant-panel { display: none; } } + +/* ── projection grid (gdrive replacement) ──────────────────────── */ +.pgrid td, .pgrid th { padding: 3px 5px; } +.pgrid .prow { background: rgba(194,65,12,0.05); } +.gcell { width: 84px; border: 1px solid var(--line); background: #fff; padding: 4px 6px; font-family: var(--mono); font-size: 11px; text-align: right; } +.gcell:focus { outline: 2px solid var(--orange); outline-offset: -1px; } +.prow .gcell { font-weight: 700; } + +/* ── tabs (PMS quarters) ───────────────────────────────────────── */ +.tabs { display: flex; gap: 0; margin-bottom: 14px; border-bottom: 2px solid var(--ink); } +.tab { background: var(--paper); border: 2px solid var(--ink); border-bottom: 0; margin-right: 6px; padding: 8px 18px; font-weight: 700; font-family: var(--mono); font-size: 12px; letter-spacing: 1px; position: relative; top: 2px; } +.tab .tab-sub { font-size: 8.5px; color: var(--muted); margin-left: 4px; letter-spacing: 0; } +.tab.on { background: var(--sheet); top: 2px; border-bottom: 2px solid var(--sheet); color: var(--orange); } + +/* ── category chips ────────────────────────────────────────────── */ +.chip { border: 1.5px solid var(--ink); background: var(--sheet); font-size: 11.5px; font-weight: 600; padding: 6px 12px; box-shadow: 2px 2px 0 rgba(16,37,54,0.14); } +.chip.on { background: var(--ink); color: #fff; } +.chip.on .muted { color: #9fb4c7; } + +/* ── kanban ────────────────────────────────────────────────────── */ +.kanban { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; align-items: start; } +.kcol { background: var(--paper); border: 1.5px solid var(--ink); padding: 10px; min-height: 120px; } +.kcol-h { font-family: var(--mono); font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--ink2); margin-bottom: 8px; } +.kcard { background: var(--sheet); border: 1.5px solid var(--ink); padding: 8px 10px; margin-bottom: 8px; cursor: pointer; box-shadow: 2px 2px 0 rgba(16,37,54,0.12); } +.kcard:hover { transform: translate(-1px,-1px); box-shadow: 3px 3px 0 rgba(16,37,54,0.18); } +.kcard-t { display: flex; justify-content: space-between; align-items: center; gap: 6px; } +.kcard-b { font-size: 12px; margin: 4px 0; } +.kcard-f { display: flex; justify-content: space-between; font-size: 9.5px; font-family: var(--mono); color: var(--muted); } + +/* ── calendar ──────────────────────────────────────────────────── */ +.cal { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; } +.cal-dow { font-family: var(--mono); font-size: 8px; text-align: center; color: var(--muted); padding: 2px 0; } +.cal-cell { border: 1px solid var(--line); min-height: 38px; padding: 2px 3px; position: relative; } +.cal-cell.empty { border-color: transparent; background: transparent; } +.cal-d { font-family: var(--mono); font-size: 9px; color: var(--muted); } +.cal-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin: 1px; border: 1px solid var(--ink); } +.cal-dot.st-ok { background: var(--ok); } .cal-dot.st-warn { background: var(--warn); } .cal-dot.st-info { background: var(--teal); } + +/* ── loaders: hatched draft linework ───────────────────────────── */ +.loader { display: flex; flex-direction: column; gap: 9px; } +.hatch { + height: 13px; border: 1px solid var(--line); + background: repeating-linear-gradient(45deg, rgba(35, 71, 107, 0.16) 0 5px, transparent 5px 10px); + background-size: 28px 28px; animation: hatchmove 0.9s linear infinite; +} +@keyframes hatchmove { to { background-position: 28px 0; } } +.loader-label { font-family: var(--mono); font-size: 9.5px; letter-spacing: 2px; color: var(--muted); text-transform: uppercase; margin-top: 2px; } +.dots i { font-style: normal; animation: dotblink 1.2s infinite; } +.dots i:nth-child(2) { animation-delay: 0.2s; } +.dots i:nth-child(3) { animation-delay: 0.4s; } +@keyframes dotblink { 0%, 60%, 100% { opacity: 0.15; } 30% { opacity: 1; } } + +/* ── typewriter caret + thinking bubble ────────────────────────── */ +.caret { color: var(--orange); animation: caretblink 0.85s steps(1) infinite; margin-left: 1px; } +@keyframes caretblink { 50% { opacity: 0; } } +.bub.thinking { display: flex; align-items: center; gap: 8px; font-family: var(--mono); font-size: 10px; letter-spacing: 1.6px; color: var(--muted); text-transform: uppercase; } + +/* ── inline text input (docs transmit etc.) ────────────────────── */ +.tin { border: 1.6px solid var(--ink); background: #fff; padding: 7px 12px; font-size: 13px; min-width: 230px; } +.tin:focus { outline: 2px solid var(--orange); outline-offset: 1px; } + +/* ── turnstile slot ────────────────────────────────────────────── */ +.captcha { margin: 4px 0 14px; min-height: 22px; } +.captcha .devnote { font-family: var(--mono); font-size: 9px; letter-spacing: 1px; color: var(--warn); border: 1px dashed var(--warn); padding: 5px 9px; display: inline-block; text-transform: uppercase; } +.bigstamp { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 30px 0 26px; text-align: center; } +.bigstamp .stamp { font-size: 15px; padding: 8px 18px 7px; transform: rotate(-6deg); } +.bigstamp code { font-size: 13px; } + +/* ── tab switcher ─────────────────────────────────────────────── */ +.tabs { display: flex; gap: 4px; } +.tab-btn { background: none; border: 1.6px solid var(--ink); padding: 6px 18px; font-family: var(--mono); font-size: 10.5px; letter-spacing: 1.2px; text-transform: uppercase; cursor: pointer; color: var(--muted); transition: background 0.12s, color 0.12s; } +.tab-btn.on { background: var(--ink); color: #fff; } +.tab-btn:hover:not(.on) { background: var(--surface); color: var(--ink); } +.badge-warn { display: inline-block; background: var(--orange); color: #fff; font-size: 9px; border-radius: 8px; padding: 0 5px; margin-left: 5px; vertical-align: middle; } + +/* ── form field helper ────────────────────────────────────────── */ +.field { display: flex; flex-direction: column; gap: 3px; font-family: var(--mono); font-size: 10px; letter-spacing: 0.8px; text-transform: uppercase; color: var(--muted); } +.field .tin { min-width: 100px; } +.ta-r { text-align: right; } +.ta-c { text-align: center; } diff --git a/apps/web/src/ui.tsx b/apps/web/src/ui.tsx new file mode 100644 index 0000000..8757c13 --- /dev/null +++ b/apps/web/src/ui.tsx @@ -0,0 +1,163 @@ +import { useEffect, useRef, useState } from 'react'; + +/** Signature components of the "drawing comes alive" design language. */ + +/** Typewriter reveal for AI responses — pen-on-paper, not a text dump. */ +export function Type({ text, speed = 12, onDone }: { text: string; speed?: number; onDone?: () => void }) { + const [n, setN] = useState(0); + const doneRef = useRef(onDone); + doneRef.current = onDone; + + useEffect(() => { + setN(0); + const id = setInterval(() => { + setN((prev) => { + const next = Math.min(prev + 2, text.length); + if (next >= text.length) { + clearInterval(id); + doneRef.current?.(); + } + return next; + }); + }, speed); + return () => clearInterval(id); + }, [text, speed]); + + return ( + <> + {text.slice(0, n)} + {n < text.length && } + + ); +} + +/** Loading state drawn as hatched placeholder linework on a sheet. */ +export function Loader({ label = 'Pulling read-models' }: { label?: string }) { + return ( +
+
+
+
+
+ + {label}... + +
+ ); +} + +/** Rubber-stamp status — rotated, double-ruled, ink-textured. */ +export function Stamp({ kind, children }: { kind: 'ok' | 'warn' | 'bad' | 'info'; children: string }) { + return {children}; +} + +/** Chainage ruler — progress drawn as a survey station strip (STA 0+000 …). */ +export function Chainage({ pct, label, tone = 'teal' }: { pct: number; label?: string; tone?: 'teal' | 'orange' }) { + const clamped = Math.max(0, Math.min(100, pct)); + const w = 220; + const h = 30; + const x = 4 + (clamped / 100) * (w - 8); + const color = tone === 'orange' ? '#c2410c' : '#0e7490'; + return ( + + + {Array.from({ length: 21 }, (_, i) => { + const tx = 4 + (i / 20) * (w - 8); + const major = i % 5 === 0; + return ; + })} + + + + {label ?? `${Math.round(clamped)}%`} + + 0+000 + 0+100 + + ); +} + +/** CCTV thumbnail — a real-looking camera tile. Shows the live snapshot when + * an NVR stream is wired (camera.online), else an honest NO-SIGNAL placeholder. */ +export function CctvThumb({ + label, + online, + streamUrl, +}: { + label: string; + online: boolean; + streamUrl?: string | null; +}) { + return ( +
+
+ {online ? '● REC' : '○ NO SIGNAL'} + {new Date().toLocaleString('en-PH', { hour12: false })} +
+ {online && streamUrl ? ( + {label} + ) : ( +
+
+
NVR NOT LINKED
+
+ )} +
+ {label} + +
+
+ ); +} + +/** Standard KPI band — identical four-card shape on every dept dashboard. */ +export function KpiBand({ + kpis, +}: { + kpis: Array<{ label: string; value: string; sub: string; tone: 'ok' | 'warn' | 'bad' | 'info' }>; +}) { + return ( +
+ {kpis.map((k) => ( +
+
{k.label}
+
+ {k.value} +
+
{k.sub}
+
+
+ ))} +
+ ); +} + +/** Drawing title block — every page header reads like a sheet legend. */ +export function SheetBar({ sheet, title, note }: { sheet: string; title: string; note?: string }) { + return ( +
+
+ SHT + {sheet} +
+
+ TITLE + {title} +
+ {note && ( +
+ NOTES + {note} +
+ )} +
+ REV + A +
+
+ DATE + {new Date().toLocaleDateString('en-PH', { year: '2-digit', month: 'short', day: '2-digit' }).toUpperCase()} +
+
+ ); +} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..a89c194 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "noEmit": true, + "useDefineForClassFields": true, + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..c315f1f --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,30 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; +import { VitePWA } from 'vite-plugin-pwa'; + +export default defineConfig({ + plugins: [ + react(), + // PWA offline rule: the shell must render without signal; field capture + // queues client-side and syncs later. + VitePWA({ + registerType: 'autoUpdate', + manifest: { + name: 'UBI Construction Suite', + short_name: 'UBI Suite', + description: 'Construction management for Ulticon Builders + Omega Asia', + theme_color: '#0f2233', + background_color: '#f3f5f7', + display: 'standalone', + icons: [ + { src: '/icon-192.png', sizes: '192x192', type: 'image/png' }, + { src: '/icon-512.png', sizes: '512x512', type: 'image/png' }, + ], + }, + }), + ], + server: { + port: 5173, + proxy: { '/api': 'http://localhost:3000' }, + }, +}); diff --git a/diagrams.html b/diagrams.html new file mode 100644 index 0000000..4ca0329 --- /dev/null +++ b/diagrams.html @@ -0,0 +1,743 @@ + + + + + +UBI Construction Suite — Functional Diagrams + + + +
+ +
+
FUNCTIONAL
VIEW · NO TECH
REV A
+
Ulticon Builders, Inc. · Omega Asia
+

Construction Suite — Functional & Data-Flow Diagrams

+
A business-level view of every department: what each one contains, how it connects to the others, and how information moves through the suite. Drawn without any technology so it can be read by management, auditors, and department heads alike.
+
+ +
+ Department module + Shared pillar (used by all) + External system + Key / priority element +
+ + +

System Block Diagram

+

The whole suite at a glance. Field and department work flows upward to the CEO’s ManCom view. Three shared pillars sit underneath everything; external systems connect at the sides.

+ +
+ + + + + + + + + + + + + CEO / Office of the President + ManCom dashboard · AI insights · trend projection · agentic view + + + CONTROL + + Operations (VPO) + approvals · subcon · health + + + Audit + findings · exceptions · oversight + + + + FIELD & DELIVERY + + + Engineering + bid·budget·qty·billing·SWA + + + Procurement + schedule·receiving·suppliers + + + Survey + volumes·stations + + + MQC + tests·certs + + + Property + QR custody + + + Field capture · photo + GPS + time → AI encode (off-box) + DR receiving · progress · billing photos + + + + SUPPORT & SERVICES + + + IT + monitor·help·capacity + + + Records + registry·expiry + + + Finance + doc batches + + + Admin / OHS + site gateway·safety + + + HR + hours engine + + + Clinic + isolated + + + Shared services: approvals · ticketing · expiry reminders · notifications + + + + FOUNDATION · USED BY EVERY MODULE + + Identity & Access + + Document Tracking + + AI-Queryable Data Layer + + + EXTERNAL SYSTEMS (systems of record) + + Acumatica + ERP · finance truth + + + Ezacctg + payroll · 201 · statutory + + + OAEC (Omega) + fleet · equipment + + + BSmart + fuel dispensers + + + + + + + + + + + + + + + + +
FIG 01 · Suite block diagram — flow rises from field/support work, through control (VPO/Audit), to the CEO. Pillars underpin all; external systems hold the record.
+
+ + +

Block Diagram per Department

+

Each department as a block: its name, what sits inside it, and the modules it connects to. IN = receives from · OUT = sends to.

+ +
+ + +
+
Engineering CORE
+
+
Inside
+
+ Bidding + Financial Planning · office & field budget + Quantity takeoff + Weekly Materials Schedule (AI-aided) + Weekly SWA + Progress photos + Billing (monthly, DPWH) + Contracts + Archiving + Pay-item library +
+
+
+
INSurvey volumes/cross-sections · MQC material certs · Operations approvals
+
OUTmaterials schedule → Procurement · equipment schedule → OAEC · SWA/billing → CEO/ManCom · transmittals → Doc Tracking
+
+
+ + +
+
Procurement
+
+
Inside
+
+ Materials schedule intake + Delivery receiving · DR photo + AI encode + Supplier performance + Cement delivery monitoring +
+
+
+
INapproved schedule from Engineering (via Operations)
+
OUTPO/expenses + encoded DR → Acumatica · follow-ups CC → Operations · performance → CEO
+
+
+ + +
+
Operations · VPO CONTROL
+
+
Inside
+
+ Approvals inbox + Subcontract registry + Project health dashboard + Standards + Issue rectification (first stop) +
+
+
+
INrequests & status from all field modules · procurement follow-ups
+
OUTapprovals back to source · vetted reports → CEO (most pass here first)
+
+
+ + +
+
Survey
+
+
Inside
+
+ Original ground + Stakeout + Cross-sections + As-built + Station / chainage index + Monthly volume +
+
+
+
OUTquantities/cross-sections → Engineering · monthly volume → CEO/ManCom
+
+
+ + +
+
MQC
+
+
Inside
+
+ DPWH minimum testing per pay item + Lab results + Pour logs + Material certificates +
+
+
+
OUTcerts → Engineering Billing · quality status → CEO/ManCom
+
+
+ + +
+
Audit OVERSIGHT
+
+
Inside
+
+ Findings / observations register + Follow-up tracking + Compliance checklists + Exception / investigation tracker +
+
+
+
INread-access across all modules
+
OUTexceptions & findings → CEO
+
+
+ + +
+
IT EXPANDING
+
+
Inside
+
+ Device monitoring + map + Firewall / endpoint status + DeskGuard v2 · activity & productivity + AI helpdesk · Kanban, auto-assign + Maintenance (PMS) scheduler + Environmental monitoring + Capacity planning & controls +
+
+
+
INactive assets from Property (for PMS)
+
OUTdevice/capacity status → CEO · shares ticketing with Admin
+
+
+ + +
+
Records
+
+
Inside
+
+ Document registry (Doc-Tracking home) + Vehicle reg / insurance / stamps + expiry + Physical location index (access-gated) +
+
+
+
INincoming docs from all departments
+
OUToutgoing docs · expiry alerts → Notifications · docs → Finance
+
+
+ + +
+
Clinic ISOLATED
+
+
Inside · encrypted, role-isolated
+
+ Pre-employment / annual medicals + Site clinic visits + Medicine issuance + Injury / incident reports + DOLE compliance +
+
+
+
OUTaggregate onlyCEO (no individual records) · sits under Admin org-wise
+
+
+ + +
+
Admin / OHS GATEWAY
+
+
Inside
+
+ Safety trainings & seminars + Toolbox meeting logs + PPE / medicine dispensing + Admin officers portal + timekeepers + Conference booking +
+
+
+
INsite inquiries/requests · houses Clinic
+
OUTbridges HR ↔ site · shares ticketing with IT · dispensing ↔ Property
+
+
+ + +
+
HR THIN
+
+
Inside
+
+ Attendance · tablet photo + PIN + Hours-computation engine + DTR + Face ID (phase 2) +
+
+
+
OUTclean hours/totals → Ezacctg (201/payroll/statutory live there)
+
+
+ + +
+
Property
+
+
Inside
+
+ Asset custody · PAR / MR + QR scan · location · status · custodian + Tool issuance / return + Warehouse custody + Monthly custodian attestation +
+
+
+
OUTactive assets → IT PMS · PPE overlap ↔ Admin
+
+
+ + +
+
Finance
+
+
Inside
+
+ Document batching / boxing → cashier + Incoming docs intake +
+
+
+
INdocs from Records & other departments
+
OUTfinance truth (PO/payments) lives in Acumatica
+
+
+ + +
+
CEO / Office of the President ENDPOINT
+
+
Inside
+
+ ManCom live dashboard + Trend projection toggle + AI insights · delays, performance + Conversational AI + Agentic iframe view +
+
+
+
INconsolidated from all departments (via Operations + AI Data Layer)
+
+
+ +
+ +
Pillars are not shown as cards because they belong to no single department — Identity & Access, Document Tracking, and the AI-Queryable Data Layer are used by every block above.
+ + +

Data-Flow Diagram — Level 0 (Context)

+

The suite as a single process, with the people and outside systems it exchanges information with.

+ +
+ + + + + + + UBI Construction + Suite + process 0 + + + + + Field staff + PE · QE · inspectors + + + Office staff + depts · clerks + + + CEO / VPO + management + + + Acumatica + ERP + + + Ezacctg + payroll + + + OAEC + Omega fleet + + + DPWH + billing recipient + + + + + + captures · photos + + + forms · docs + + + ManCom · insights + + + encoded DR + + + hours + + + equipment sched. + + + monthly billing + + +
FIG 03 · Context diagram — who feeds the suite and what it returns. Arrows are information, not paper.
+
+ + +

Data-Flow Diagram — Level 1

+

Inside the suite: the major processes, the stores that hold information, and how a capture becomes a delivery record, an accomplishment, a billing, and finally a ManCom figure.

+ +
+ + + + + + + + + Field staff + + CEO / VPO + + + + 1 · Field capture + photo·GPS·time·encode + + + + 2 · Quantity & SWA + measure·accomplish + + + + 3 · Procurement + schedule·receive + + + + 4 · Billing + monthly·to DPWH + + + + 5 · Document routing + transmittal·track + + + + 6 · Consolidation + ManCom·AI insights + + + + + D1 · Deliveries / DR + + D2 · Pay items & budgets + + D3 · SWA / accomplishment + + D4 · Quality & survey + + D5 · Billings + + D6 · Document register + + D7 · Assets & custody + + + + + Acumatica + + DPWH + + + + capture + + + DR detail + + + store D1 + + + + + + photos + + + SWA ref + + + read D2 + + + store D3 + + + read D4 + + + store D5 + + + attach + + + store D6 + + + route to cashier + + + billing → DPWH + + + + + + + read D1–D7 + + + dashboard + +
FIG 04 · Level-1 DFD — circles are processes, blue boxes are stores (D1–D7), dashed boxes are external. A capture (P1) becomes a delivery (D1), feeds quantities/SWA (P2→D3), drives billing (P4→D5), routes as documents (P5→D6), and rolls up to ManCom (P6).
+
+ +
+ Ulticon Builders, Inc. — Construction Suite · Functional & DFD set · Rev A
+ Companion to the Claude Code context pack (CLAUDE.md + per-module files). Open in any browser; use Print → Save as PDF for a PDF copy. +
+ +
+ + diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..dfe6974 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,21 @@ +# Local dev parity with the Lightsail box: Postgres + Redis as external +# endpoints (hard rule 6). The API itself runs with `npm run dev:api`. +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: ubi + POSTGRES_PASSWORD: ubi_dev + POSTGRES_DB: ubi_suite + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + +volumes: + pgdata: diff --git a/mockups.html b/mockups.html new file mode 100644 index 0000000..126a089 --- /dev/null +++ b/mockups.html @@ -0,0 +1,791 @@ + + + + + +UBI Construction Suite — CEO Module UI Mockups + + + +
+ +
+
UI MOCKUP
CEO MODULE
REV A
+
Ulticon Builders, Inc. · Omega Asia · Construction Suite
+

CEO / Office of the President — UI Mockups

+
+ Annotated design frames for the suite's primary output: the ManCom dashboard and its + companions — AI Insights, Ask AI (conversational), the Agentic View host shell, + and the mobile ManCom. Drawn in the brand light UI (navy chrome · orange action · teal data). + Numbered orange dots 1 in each frame match the notes + beside it. Open in any browser; Print → Save as PDF to share. +
+
+ + +

Design Language

+

One palette across documents and product: the same navy / orange / teal as diagrams.html, applied as a clean light app UI. Conventions every screen reuses.

+ +
+
Ink navy#0F2233 chrome · headers
+
Flag orange#C2410C primary action · alerts
+
Data teal#0E7490 charts · evidence
+
Pillar blue#1D4ED8 AI · read-model tags
+
OK green#15803D on-time · ahead
+
Warn amber#B45309 pending · lagging
+
Bad red#B91C1C slippage · escalated
+
App canvas#F3F5F7 white cards on soft grey
+
+ +
+ + + + + + on-time + pending 6d + escalated + aggregate only + + SRC · engineering.swa + DR log wk22–24 + → −2.9% Jul (proj.) +
+
FIG 00 · Core components — buttons, status chips, a read-model source tag (blue), an evidence chip (teal), and a projection chip (dashed orange, shown only when Trend Projection is ON).
+ +
Three conventions repeated everywhere:solid = actual, dashed = projected, grey = plan on every chart and chip; ② every widget carries a blue SRC tag naming the Pillar-3 read-model it consumes — the dashboard doubles as the data-contract spec; ③ AI output is always grounded: an insight or chat answer without evidence chips does not ship.
+ + +

ManCom — Live Dashboard

+

The President's landing page. Everything every department produces rolls up here through the AI-queryable layer; Operations vets most of it on the way. KPI band → trend charts → flagged projects + report feed, with AI riding in the right rail.

+ +
+
+
+
suite.ulticon.ph/ceo/mancom
+
+ + +
+
UBI SuiteConstruction
+
Entity UBI ▾ 1
+
Office of the President
+
ManCom
+
AI Insights 3
+
Ask AI
+
Agentic View
+
Departments
+
Operations
+
Engineering
+
Procurement
+
Survey
+
MQC
+
Audit
+
+ 8 more…
+
EC
E. CastilloPresident & CEO · cross-entity read
+
+ + +
+
+

ManCom

+
MayJune 2026 ▾
+
+ Project trends forward 2 + live · as of Jun 11, 10:42 +
+
+ +
+
Active projects
104 6 ⚑
98 DPWH pkgs · 6 flagged by Operations
+
Billings — June
₱182.4M ▲ 12%
21 of 34 monthly drafts submitted
+
Avg slippage
−2.31% → −2.9% Jul 3
3 projects below −5%
+
Open exceptions
7 2 escalated
Audit investigation tracker
+
+ +
+
+
Accomplishment vs plan — cumulative ₱M 4 SRC · engineering.swa+billing
+ + + + + + 0 + 600 + 1.2k + + + TODAY + + + + + + + + + + + gap widens + + JanFeb + MarApr + MayJun + JulAug + + + +
+ plan + actual + projected (3-mo trend) +
+
+ +
+
Survey volumes — monthly m³ ×1000 SRC · survey.volumes
+ + + + + + 0 + 40 + 80 + + + + + + + + to date + + proj. + + + JanFeb + MarApr + MayJun + Jul + + + +
+ reported + month to date + projected +
+
+
+ +
+
+
Flagged projects 5 SRC · operations.health
+ + + + + + + + + + + + + + + + + + + + +
ProjectPE / PMSlipTrendProj. Jul
Pkg 5 · Daang Maharlika Rehab K1440dela Cruz / Uy−4.6%−6.2%
Pkg 2 · Iligan Coastal DiversionRamos / Lim−1.2%−0.8%
Pkg 11 · Bukidnon Boundary RdSantos / Go+3.1%+3.4%
+
+ +
+
Department report feed 6 SRC · *.report-status
+ + + + + + + + +
Engineering — SWA wk 24on-time
Survey — May volumesreceived
MQC — certs for Billing No. 72 pending · 6d
Procurement — DR sync14 today
Operations — approvals queue8 waiting
Audit — exceptions2 escalated
Clinic — June health summary 7aggregate only
+
+
+
+ + +
+
AI Insights 8 view all →
+
● CRITICAL

Pkg 5 trends to −6.2% by July. Cement deliveries late 3 straight weeks; Item 311 SWA gap growing.

Proc · DR logEng · SWA 22–24
+
● WARNING

Billing No. 7 (Pkg 2) blocked — 2 MQC certs pending 6 days.

MQC · certsEng · billing
+
● POSITIVE

Pkg 11 ahead 6 straight weeks — billing could move earlier.

Eng · SWA
+
+
Ask AI 9
+

"Why is Package 5 slipping?" — answered 09:12

+
Ask about any project or department…
+
+
+ +
+
+
FIG 01 · ManCom live dashboard — desktop, 1280px class. Trend projection ON.
+
+ +
+

Design notes

+
    +
  1. 1Entity switcher. UBI / Omega Asia / All-consolidated. Every query below is entity-scoped (hard rule); "All" is a CEO-only privilege.
  2. +
  3. 2Trend-projection toggle — the spec's checkbox. ON extends every chart forward on recent trend and reveals dashed "proj." chips. One control governs the whole page.
  4. +
  5. 3Projection chips use dashed orange borders so forecast is never confusable with fact.
  6. +
  7. 4Charts: grey = plan, teal solid = actual, orange dashed = projected. The red "gap widens" marker is the negative-slippage early warning from the spec.
  8. +
  9. 5Flagged projects come pre-vetted from Operations (first-stop rectification) — the CEO sees the residue, not the noise.
  10. +
  11. 6Report feed tracks each department's submission status; "late report" rows are exactly what the AI-insights agent feeds on.
  12. +
  13. 7Clinic row is aggregate-only, enforced by RBAC at the read-model — never individual medical detail.
  14. +
  15. 8AI rail shows top-3 by severity, each grounded with teal evidence chips. Full list lives in the AI Insights page (FIG 03).
  16. +
  17. 9Ask-AI dock is persistent — one tap from any KPI to a conversation (FIG 04).
  18. +
+
+
+ + +

Trend Projection — OFF vs ON

+

The same chart in both toggle states. OFF is the factual record; ON answers "where does this trajectory land?" by extending the last three months of trend — the spec's "negative slippage next month" call-out made visual.

+ +
+
+
+
Projection OFF — history only off
+ + + + + + Jun (today) + +
+
+
Projection ON — trajectory revealed on
+ + + + + + + + + −slippage + +
+
+
+

Design notes

+
    +
  1. 1One global toggle, not per-chart — the CEO flips one switch and the entire page leans forward.
  2. +
  3. 2Method is honest and simple: linear extension of the last 3 months (accrual/forecast). The tooltip names the method; no black-box forecasting in v1.
  4. +
  5. 3State persists per user. Print/export renders whichever state is showing — useful for the physical ManCom deck.
  6. +
+
+
+ + +

AI Insights — Full View

+

The auto-surfaced register: delays, good/poor performance, bottlenecks. Generated off-box by the insights agent reading every module's Pillar-3 read-model; each card is grounded, severity-ranked, and routes into an existing workflow.

+ +
+
+
+
suite.ulticon.ph/ceo/insights
+
+
+
+

AI Insights

+
last run 07:00 · next 13:00 · run now 1
+
+
+ All (9)Critical (1)Warning (5)Positive (3) + + All departments ▾This week ▾ +
+ +
+
CRITICALPkg 5 trends to −6.2% slippage by JulySRC · cross-module
+
Cement deliveries from Solid Mix Corp arrived late 3 consecutive weeks (avg 2.4 days). PCCP pours on Item 311 are 1,180 m² behind program; the weekly SWA gap has widened each week since wk 22. At current trend the package crosses the −5% DPWH attention threshold in the first week of July. 2
+
Procurement · DR log wk22–24Engineering · SWA wk22–24Survey · vol Δ STA 4+200–5+600
+
3
+
generated 07:00 by insights agent (off-box) · confidence HIGH · model run #214
+
+ +
+
WARNINGBilling No. 7 (Pkg 2) blocked by MQC certs
+
2 material certificates (Item 311 beam tests) pending 6 days. Billing draft is otherwise complete — certs are the only missing attachment.
+
MQC · cert queueEngineering · billing №7Doc-Tracking · DOC-2026-04187
+
+
+ +
+
WARNING4 vehicle registrations expire within 15 days
+
Records expiry monitor: 3 dump trucks (Pkg 5 haul fleet) + 1 service vehicle. Renewal lead time averages 9 days.
+
Records · expiry register
+
+
+ +
+
POSITIVEPkg 11 ahead of plan 6 consecutive weeks 4
+
+3.1% and climbing; subbase complete 3 weeks early. Consider advancing Billing No. 4 and reallocating one grader to Pkg 5.
+
Engineering · SWAOperations · health
+
+
+
+
+
+
FIG 03 · AI Insights register — severity-filtered list; every card grounded and actionable.
+
+ +
+

Design notes

+
    +
  1. 1Off-box cadence is visible. The agent runs on the HCI/workstation schedule (07:00 / 13:00); "run now" queues a fresh pass. The API box never does inference (hard rule 1).
  2. +
  3. 2Narrative + numbers. Each insight reads as one paragraph a President can forward verbatim, with the quantified cause chain inside.
  4. +
  5. 3Actions route into existing workflows — VPO inbox, Audit exception register, module deep-links. Insights never dead-end.
  6. +
  7. 4Good news is first-class. The spec asks for good and poor performance; positive cards carry reallocation suggestions, not just praise.
  8. +
  9. 5Dismissals are logged (who/when) so the Audit module can review what leadership chose to ignore.
  10. +
+
+
+ + +

Ask AI — Conversational View

+

The CEO's "what's happening?" box. The agent answers across the whole suite by querying each module's read-model — every claim cited, every answer RBAC-scoped.

+ +
+
+
+
suite.ulticon.ph/ceo/ask
+
+
+

Ask AI

scope: cross-entity read · clinic aggregate-only 1
+
+
What's happening?
+
+ Across 104 active projects today: 2 +
    +
  • Billing pace is healthy — ₱182.4M drafted, 21/34 submitted, tracking +12% vs May.Eng
  • +
  • Pkg 5 is the main risk — late cement 3 straight weeks, projected −6.2% by July.ProcEngSurvey
  • +
  • 8 approvals waiting at VPO — oldest is a Pkg 7 subcontract, 4 days.Ops
  • +
+
answered from 9 read-models · 10:42 · agent off-box · sources expandable
+
+
Why is Package 5 slipping?
+
+ Three linked causes: Solid Mix Corp delivered cement late 3 consecutive weeks (avg 2.4 days)Proc · DR → PCCP pours on Item 311 fell 1,180 m² behind programEng · SWA → measured volume at STA 4+200–5+600 confirms the gapSurvey. Procurement has flagged the supplier; an alternate is under canvass. 3 +
open: DR log · SWA wk24 · supplier scorecard
+
+
4Which billings are at risk this month?Top 5 suppliers by delayApprovals older than 3 days
+
Ask anything across the suite…
+
+
+
+
+
FIG 04 · Ask AI — grounded conversational answers over the Pillar-3 query surface.
+
+ +
+

Design notes

+
    +
  1. 1Scope banner is permanent. Answers are generated under the caller's RBAC; the clinic guard (aggregate-only) is enforced at the read-model, and the banner says so.
  2. +
  3. 2"What's happening?" returns a digest, not a dump — three decisions-worth of signal, each line citing its source modules with teal chips that expand to raw records.
  4. +
  5. 3Cause chains cite each hop. Delivery → pour → measured volume mirrors how the data actually flows between modules (DFD P1→P2).
  6. +
  7. 4Suggested prompts teach the surface. Seeded from what the read-models can actually answer well — they double as the query-layer test suite.
  8. +
  9. 5Same agent, three skins: the rail dock (FIG 01), this page, and mobile (FIG 06) hit one conversation API.
  10. +
+
+
+ + +

Agentic View — Sandboxed Host Shell

+

The "statically dynamic" page: an off-box agent generates fresh dashboard HTML on its own schedule; the host shell loads it in a strict sandbox. The frame below mocks the host contract — the agent-generated content inside is deliberately styled differently, because the agent owns that design.

+ +
+
+
+
suite.ulticon.ph/ceo/agentic
+
+
+
+

Agentic View

+
+ generated today 07:00 by mancom-agent 1 + +
+
+
2 + sandboxed iframeno external network6 allowed read endpointsRBAC: ceo scopeartifact #88 · sha-pinned +
+
+ AGENT-GENERATED LAYOUT · LOADED ARTIFACT +
3 +
Today the story is cement.
+
₱182.4M billed · 1 package at risk
+
+
The riskPkg 5 loses ~₱9.7M of July billing if the cement gap holds. Alternate supplier canvass closes Friday.
+
Worth praisingPkg 11 crew — 6 straight weeks ahead. Santos / Go.
+
Waiting on you2 escalated exceptions; Pkg 7 subcon at VPO 4 days.
+
+
+
+
host contract: fetch signed artifact → render in sandbox (no scripts beyond allowlist, no external fetch) → data via read-only Pillar-3 endpoints · layout may change every run by design 4
+
+
+
+
FIG 05 · Agentic view — host shell fixed, content free. Detailed agent design is a separate session per the kickoff context.
+
+ +
+

Design notes

+
    +
  1. 1Provenance up front: which agent, when, which artifact. "Regenerate" queues an off-box run — never on the API box.
  2. +
  3. 2The sandbox strip is the contract, human-readable: iframe sandboxing, zero external network, an explicit allowlist of read-only endpoints, CEO-scoped RBAC.
  4. +
  5. 3Inside the dashed border the agent owns the design — here it chose an editorial "morning briefing" style. The mock intentionally breaks the suite's UI language to make the boundary obvious.
  6. +
  7. 4Artifacts are versioned + sha-pinned; History lets the CEO reopen yesterday's view, and a bad generation is one click backwards.
  8. +
+
+
+ + +

ManCom on Mobile

+

The suite is mobile-first and the President is not always at a desk. Same read-models, condensed: KPI grid, insight feed, Ask-AI dock. Read-mostly by design — deep work links back to desktop views.

+ +
+
+
+
+
10:42LTE ▮▮▮
+
+
ManCom 1UBI ▾
+
trends fwdlive · 10:42
+
+
+
+
Projects
104 6⚑
6 flagged
+
Billings
₱182M
▲ 12% vs May
+
Slippage
−2.31%
→ −2.9% Jul (proj.)
+
Exceptions
7
2 escalated
+
+
AI Insights 2
+
● CRITICAL

Pkg 5 → −6.2% by July. Cement late 3 wks; Item 311 gap growing.

+
● WARNING

Billing №7 blocked — 2 MQC certs pending 6 days.

+
● POSITIVE

Pkg 11 ahead 6 weeks — advance billing?

+
3 Ask AI — what's happening?
+
+
+ ManComInsightsAsk AIMore +
+
+
+
+ +
+

Design notes

+
    +
  1. 1Same toggle, same entity switcher — projection state and entity scope sync with desktop per user.
  2. +
  3. 2Insight cards are tappable → full card with evidence and actions; severity color is the left border, readable in sunlight.
  4. +
  5. 3Ask AI is thumb-reach docked. Voice input matters here — the CEO talks to it from the car between site visits.
  6. +
  7. 4PWA offline rule applies: last-fetched ManCom renders offline with a stale-data banner; never a blank screen.
  8. +
+
+
+ + +

Widget → Read-Model Contract

+

Every widget above names its source. This table is the same information as a build checklist — the CEO module ships last (phase 8), but these Pillar-3 read-model shapes must be designed into each module from its first migration.

+ + + + + + + + + + + +
WidgetSource read-modelCadenceNotes
KPI bandengineering.billing operations.health audit.exceptionslive / 5 minSlippage projection computed server-side from the same trend used by charts.
Accomplishment vs planengineering.swa engineering.billingweekly + on submitPlan from financial-planning budgets; actual from weekly SWA; ₱ via pay-item rates.
Survey volumessurvey.volumesmonthlyThe ManCom monthly volume feed named in the Survey spec.
Flagged projectsoperations.healthliveOperations vets first (first-stop rectification) — flags arrive pre-triaged.
Report feed*.report-statusliveUniform shape every module exposes: report, due, submitted, status. Feeds the "delayed reports" insight.
AI Insightsall read-models → insights agent07:00 / 13:00 + on demandAgent runs off-box (HCI/workstation), posts results back. Box stores and serves only.
Ask AIpillar3.queryon demandRBAC-aware query surface; clinic exposes aggregates only at the model, not the UI.
Agentic viewagent.artifacts + 6 read endpointsdaily + regenerateSigned HTML artifact, sha-pinned, rendered in sandbox. Host contract per Pillar 3.
+
FIG 07 · The dashboard as data contract — if a module can't fill its row, the gap is visible before phase 8.
+ + +

Out of Scope This Round / Next Mockups

+

This set covers the CEO module only, per scope decision. Natural next rounds, in suite-phasing order:

+
+ Next mockup sessions → ① Field mobile set: login + offline session, PE home, AI-prefilled weekly materials schedule, DR receiving capture (branch engineering_materials_weekly-schedule-ai); ② Document Tracking: scan-to-transmit / receive + "where is this paper?" console (branch pillar-doc-tracking); ③ VPO approvals inbox (branch operations); ④ IT module: SNMP map, DeskGuard v2 with its mandatory consent banner, helpdesk Kanban, capacity forecasts (branch it). The agentic-view content design stays a separate session per the kickoff context. +
+ +
+ Ulticon Builders, Inc. — Construction Suite · CEO module UI mockups · Rev A
+ Companion to CLAUDE.md (kickoff context) + diagrams.html (functional & DFD set). Open in any browser; Print → Save as PDF for a PDF copy. +
+ +
+ + diff --git a/ops/DEPLOY.md b/ops/DEPLOY.md new file mode 100644 index 0000000..bc2f5c9 --- /dev/null +++ b/ops/DEPLOY.md @@ -0,0 +1,91 @@ +# DEPLOY.md — Server Claude deploy + auto-fix playbook + +> You are **Server Claude**, running on the production box (`edge.ubi-as.com`). +> You were spawned headlessly to deploy the `production` branch. Follow this +> playbook exactly, **fix problems yourself**, and never leave the site down. +> When done, write `ops/DEPLOY_STATUS.md` and commit + push it. + +## Hard safety rules (do not violate) +1. **Never `DROP DATABASE` / never delete the Postgres volume in production.** Demo + phase or not, the cutover plan says real data may arrive; treat the DB as precious. + Schema changes happen via TypeORM migrations or `DB_SYNC` only. +2. **Never commit or print secrets.** `.env` stays on the box, untracked. +3. **Never force-push.** Never touch `main`. You only read `production` and push + `ops/DEPLOY_STATUS.md` + `ops/DEPLOY_LOG.md` + state files back to `production`. +4. **Bounded fixing:** try at most **3** fix attempts for a given failure. If still + broken, **roll back** (see §5) and report FAILED — do not keep flailing. +5. If a step needs a capability you don't have permission for, stop and report it in + the status file rather than working around it. + +## 0. Preflight +- Repo path: the directory containing this file's parent (`ops/..`). `cd` there. +- Record the currently-deployed SHA as the rollback target: + `LAST_GOOD=$(cat ops/.last_good 2>/dev/null || git rev-parse HEAD)` +- Read `ops/RELEASE_NOTES.md` if present — Cloud Claude leaves deploy-specific + notes there (new env vars, migration steps, reseed instructions). Honor them. + +## 1. Sync +- `git fetch origin production` +- `TARGET=$(git rev-parse origin/production)` +- If `TARGET` == `cat ops/.last_deployed` → nothing to do; exit 0 (write a short + "no change" status only if invoked manually). +- `git checkout production && git reset --hard origin/production` + +## 2. Build +- `npm install` (new deps land often — never skip). +- `npm run build` (builds packages → api → web). If it fails: + - Read the error. Common causes: stale `@ubi/types` dist (run + `npm run build -w @ubi/types` then retry), a TS error (fix the obvious cause or, + if it's clearly a bad push, roll back), missing dep (`npm install`). + +## 3. Environment (verify, don't overwrite) +- Confirm `.env` has: `NODE_ENV=production`, a real `JWT_SECRET` (not `change-me`), + `CORS_ORIGIN=https://edge.ubi-as.com`, `SEED_DEMO` set intentionally + (demo phase = `true`; once real data is in = `false`). +- Apply any env changes RELEASE_NOTES.md asks for. If a required var is missing, + stop and report — do not invent secrets. + +## 4. Migrate / restart +- DB: with `DB_SYNC=true`, new tables/columns auto-create on boot — fine for demo. + If RELEASE_NOTES.md says "run migration", run `npm run migration:run` (when it exists). +- Restart the API: **`systemctl restart ubi-edge`** (the service binds + 127.0.0.1:8091 behind nginx on this box). Confirm it came up with + `systemctl status ubi-edge` / `journalctl -u ubi-edge -n 50`. Web is static + (`apps/web/dist`) served by nginx — no restart needed unless nginx config changed. + (If the unit name ever changes, discover it: `systemctl list-units | grep -i ubi`.) + +## 5. Health check (the gate) +- `curl -fsS https://edge.ubi-as.com/api/health` must return `{"status":"ok"...}`. +- `curl -fsS -o /dev/null -w '%{http_code}' https://edge.ubi-as.com/` must be 200. +- Spot-check the new build is actually live (e.g. an endpoint added in this release). +- **If health fails after up to 3 fix attempts: ROLL BACK** + `git reset --hard $LAST_GOOD && npm install && npm run build && `, + re-run health check, and mark the deploy **FAILED (rolled back)**. + +## 6. Record state + report (always do this last) +- On success: `git rev-parse HEAD > ops/.last_deployed` and `cp ops/.last_deployed ops/.last_good`. +- Write `ops/DEPLOY_STATUS.md` from the template in §7. +- Append a one-line entry to `ops/DEPLOY_LOG.md` (create if missing): + `- · · · ` +- Commit just those files and push: + `git add ops/DEPLOY_STATUS.md ops/DEPLOY_LOG.md ops/.last_deployed ops/.last_good` + `git commit -m "ops(deploy): " && git push origin production` + (Use the box's configured git credentials; never embed a token in the message.) + +## 7. DEPLOY_STATUS.md template +``` +# Deploy status +- result: OK | FAILED | ROLLED-BACK | NO-CHANGE +- deployed_sha: +- previous_sha: +- at: +- health: api= web= +- build: clean | "" +- fixes_applied: + - +- errors: +- for_cloud_claude: +``` + +Keep `for_cloud_claude` honest and specific — it's how the two of us close the loop. diff --git a/ops/DEPLOY_STATUS.md b/ops/DEPLOY_STATUS.md new file mode 100644 index 0000000..1399b72 --- /dev/null +++ b/ops/DEPLOY_STATUS.md @@ -0,0 +1,12 @@ +# Deploy status + +- result: NOT-YET-DEPLOYED +- deployed_sha: — +- previous_sha: — +- at: — +- health: api=— web=— +- build: — +- fixes_applied: — +- errors: — +- for_cloud_claude: Server Claude overwrites this file after each deploy. Cloud + Claude reads it at the start of a session to learn how the last deploy went. diff --git a/ops/INSTALL-AUTOMATION.md b/ops/INSTALL-AUTOMATION.md new file mode 100644 index 0000000..8c9e778 --- /dev/null +++ b/ops/INSTALL-AUTOMATION.md @@ -0,0 +1,85 @@ +# INSTALL-AUTOMATION.md — stand up Claude-driven CD on the box + +> Server Claude: install **both** triggers — the **webhook** (instant) and the +> **timer** (1-min backstop). Both call the same `ops/wake-claude-deploy.sh`, +> which spawns a headless `claude -p` deploy session (`ops/DEPLOY.md`). Report +> back in `ops/DEPLOY_STATUS.md` when done. + +Assumes: repo at `/srv/ubi`, deploy user `deploy` (adjust paths/user to match this +box — the live API unit is `ubi-edge`). `claude`, `node`, `npm`, `git` on PATH for +that user, and `claude` already authenticated for it. + +## 0. Permissions (so headless runs don't hang on prompts) +```bash +cd /srv/ubi +cp ops/deploy-box.settings.json .claude/settings.json # tool allowlist (ubi-edge restart, git, npm, psql, curl) +``` + +## 0.5 Copy triggers OUTSIDE the repo (loop-guard survival) +The deploy does `git reset --hard origin/production`, which would revert any +in-repo trigger edits mid-run. So the systemd units run **guarded copies** from +`/opt/ubi-deploy/`. Re-copy whenever these two files change in the repo. +```bash +sudo mkdir -p /opt/ubi-deploy +sudo cp ops/wake-claude-deploy.sh ops/webhook-receiver.mjs /opt/ubi-deploy/ +sudo chmod +x /opt/ubi-deploy/wake-claude-deploy.sh +``` +The wake script carries a **loop guard**: it never deploys when the tip commit is +an `ops(deploy): …` status commit (otherwise each deploy's status push would +re-trigger a deploy forever). Verify after install: a status-commit push must be +skipped, a real change must deploy. +The wake script uses `--permission-mode acceptEdits --allowedTools ...`; the +allowlist must cover everything `DEPLOY.md` needs. If a deploy is denied a command, +add it to the allowlist and note it in `for_cloud_claude:`. + +## 1. Timer (backstop, zero inbound surface) +```bash +sudo cp ops/systemd/ubi-deploy.{service,timer} /etc/systemd/system/ +# edit the unit: set User=, WorkingDirectory=/srv/ubi, UBI_REPO, PATH to match this box +sudo systemctl daemon-reload +sudo systemctl enable --now ubi-deploy.timer +systemctl list-timers ubi-deploy.timer # confirm it's scheduled +``` + +## 2. Webhook (instant, primary) +```bash +# a) secret + env file (chmod 600 so only root/deploy reads it) +SECRET=$(openssl rand -hex 32) +printf 'GH_WEBHOOK_SECRET=%s\n' "$SECRET" | sudo tee /etc/ubi-webhook.env >/dev/null +sudo chmod 600 /etc/ubi-webhook.env +echo "SECRET for GitHub: $SECRET" # you'll paste this into GitHub + +# b) run the receiver (binds 127.0.0.1:8787 — never exposed directly) +sudo cp ops/systemd/ubi-webhook.service /etc/systemd/system/ +# edit unit: User=, WorkingDirectory, UBI_REPO, PATH +sudo systemctl daemon-reload +sudo systemctl enable --now ubi-webhook +curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8787 # 405 to GET = up + +# c) nginx route (see ops/nginx.sample.conf): add inside the edge.ubi-as.com server +# location = /ubi-deploy-hook { proxy_pass http://127.0.0.1:8787; } +sudo nginx -t && sudo systemctl reload nginx +``` +Then in **GitHub → repo → Settings → Webhooks → Add webhook**: +- Payload URL: `https://edge.ubi-as.com/ubi-deploy-hook` +- Content type: `application/json` +- Secret: the `$SECRET` printed above +- Events: **Just the push event** +- Active: yes + +(Optional hardening: restrict the nginx location to GitHub's hook source ranges +from `https://api.github.com/meta` → `.hooks[]`.) + +## 3. Verify end-to-end +- Ask the owner (Cloud Claude) to **"build production"** with a trivial change. +- Webhook path: GitHub shows a green delivery; `journalctl -u ubi-webhook -f` + logs "waking deploy"; a new `ops/DEPLOY_STATUS.md` is pushed within ~1–2 min. +- Timer path (disable webhook to test): within 1 min of a `production` change, + `journalctl -u ubi-deploy.service` shows a run. +- Both: `git log origin/production` shows your `ops(deploy): OK` commit. + +## 4. Guardrails (already in place — confirm) +- `flock` in the wake script prevents overlapping deploys (webhook + timer can't collide). +- `DEPLOY.md`: never drops DB, never force-pushes, rolls back on failed health check. +- Receiver verifies HMAC; rejects unsigned/forged payloads (401). +- If anything can't be installed safely, stop and report in `DEPLOY_STATUS.md`. diff --git a/ops/PRE-PROD-AUDIT.md b/ops/PRE-PROD-AUDIT.md new file mode 100644 index 0000000..0b6d45f --- /dev/null +++ b/ops/PRE-PROD-AUDIT.md @@ -0,0 +1,34 @@ +# PRE-PRODUCTION AUDIT CHECKLIST + +> Surfaced before any **real production launch** (referenced from `CLAUDE.md §0`). +> The suite is in POC/pilot mode with intentional shortcuts; clear these first. +> When the owner says "audit before production," walk this top to bottom. + +## 🔴 Secrets to rotate (all exposed during the pilot) +- [ ] **Cartrack API key** — `CARTRACK_PASS` (admin, read+write) is in git history + (`ops/RELEASE_NOTES.md`) and chat. Rotate in Fleetweb → ideally issue a + **read-only / standard** API user for the suite. Update on box + RELEASE_NOTES. + Consider purging it from git history (filter-repo) since both repos hold it. +- [ ] **GitHub PAT** — pasted in chat repeatedly during the session. Rotate. +- [ ] **Shared password `Ulticon1`** — every account uses it. Set strong, unique + per-account passwords (regenerate `apps/api/src/seed/credentials.ts`). +- [ ] Move box secrets OUT of `ops/RELEASE_NOTES.md` (committed) into the box + `.env` / a real secret store; clear the committed values. + +## 🟠 Data & hardening +- [ ] **TypeORM migrations** — replace `DB_SYNC=true` with real migrations, then + set `DB_SYNC=false` (before any data you can't recreate). +- [ ] **`SEED_DEMO=false`** on prod once real data is in; disable/remove demo + accounts (ceo@ubi.ph etc.) and the demo-seed data. +- [ ] **helmet** security headers on the API. +- [ ] **Backups** — Postgres + `uploads/` (and a tested restore). +- [ ] **Clinic** module — separate encrypted DB / field-level encryption (per spec; + currently only an aggregate stub) before any real medical data. +- [ ] **Turnstile** keys live for the public helpdesk if exposed more widely. +- [ ] Re-enable / verify search-engine block stays until intended public launch. + +## 🟡 Access & review +- [ ] Review RBAC claims per real user; remove pilot blanket access. +- [ ] Confirm clinic data is aggregate-only everywhere it surfaces (CEO/DOLE). +- [ ] Rate-limit + abuse review on public endpoints (helpdesk, login). +- [ ] Audit-log review (who can see physical-location index, etc.). diff --git a/ops/README.md b/ops/README.md new file mode 100644 index 0000000..14e92a1 --- /dev/null +++ b/ops/README.md @@ -0,0 +1,78 @@ +# ops/ — Claude-driven deployment + +**Goal:** Cloud Claude (in the web/CLI session) pushes a reviewed release; **Server +Claude** (a headless `claude -p` run on `edge.ubi-as.com`) deploys it and *fixes +deployment issues itself*. Status flows back through a committed markdown file. + +Claude does the deploy — not a script — so it can diagnose and repair failures. +The only scripts here are the dumb trigger and a rollback safety net. + +## The flow +1. **Cloud side (here):** when you say *"build production"*, Cloud Claude + fast-forwards the **`production`** branch to the reviewed commit and pushes it, + updating `ops/RELEASE_NOTES.md` if the deploy needs special steps. +2. **Trigger (on the box):** either the **webhook** (instant) or the **timer** + (~1 min) notices `production` changed and runs `ops/wake-claude-deploy.sh`. +3. **Server Claude:** `claude -p "Read ops/DEPLOY.md and execute it"` — pulls, + builds, restarts, health-checks, **fixes or rolls back**, then writes & pushes + `ops/DEPLOY_STATUS.md`. +4. **Cloud side:** next session, Cloud Claude reads `ops/DEPLOY_STATUS.md` and acts + on `for_cloud_claude:` (e.g. fix a build step so the next deploy is clean). + +``` +build production ─▶ push `production` ─▶ trigger ─▶ claude -p DEPLOY.md ─▶ DEPLOY_STATUS.md ─▶ read back +``` + +## Files +| File | Who | Purpose | +|------|-----|---------| +| `DEPLOY.md` | Server Claude | The deploy + auto-fix + rollback playbook it executes | +| `wake-claude-deploy.sh` | trigger | Detects change, spawns headless Claude (`--force` from webhook) | +| `webhook-receiver.mjs` | Option A | GitHub push→deploy listener (HMAC-verified) | +| `systemd/ubi-deploy.{service,timer}` | Option B | Poll `production` every minute | +| `systemd/ubi-webhook.service` | Option A | Keep the webhook receiver running | +| `deploy-box.settings.json` | box | Copy to `/.claude/settings.json` — tool allowlist | +| `RELEASE_NOTES.md` | Cloud Claude | Per-release instructions (env, migrations) | +| `DEPLOY_STATUS.md` | Server Claude | Result of the last deploy (read by Cloud Claude) | +| `DEPLOY_LOG.md` | Server Claude | Append-only one-line history | + +## One-time setup on the box +```bash +# 0. checkout + deploy user (example path /srv/ubi, user `deploy`) +sudo -u deploy git clone /srv/ubi && cd /srv/ubi +git checkout production +cp ops/deploy-box.settings.json .claude/settings.json # tool allowlist +# ensure `claude` is installed & authenticated for the deploy user, `node`/`npm` on PATH + +# Option B — polling timer (simplest, no inbound port): +sudo cp ops/systemd/ubi-deploy.{service,timer} /etc/systemd/system/ +sudo systemctl enable --now ubi-deploy.timer + +# Option A — instant webhook (the box already serves HTTPS): +echo 'GH_WEBHOOK_SECRET=' | sudo tee /etc/ubi-webhook.env && sudo chmod 600 /etc/ubi-webhook.env +sudo cp ops/systemd/ubi-webhook.service /etc/systemd/system/ +sudo systemctl enable --now ubi-webhook +# nginx: proxy_pass /ubi-deploy-hook → http://127.0.0.1:8787 ; +# GitHub repo → Settings → Webhooks → https://edge.ubi-as.com/ubi-deploy-hook, +# content-type application/json, secret = GH_WEBHOOK_SECRET, event = "push" only. +``` +Run **both** if you like: webhook for instant deploys, timer as a safety net that +catches any missed event. + +## Recommendation +Start with **Option B (timer)** — zero inbound surface, matches your "check every +minute" idea, works today. Add **Option A (webhook)** once you want instant deploys; +same `DEPLOY.md` brain, so nothing else changes. + +## Why not cloud Routines / plain GitHub Actions? +- **Routines** run on Anthropic infra — they can't restart services on your box, so + they can't be the deployer (could only *trigger* the box's webhook). +- **GitHub Actions** would run Claude in a runner, not on the prod box; you'd need a + self-hosted runner *on* the box to restart services. The timer/webhook + `claude -p` + is simpler and keeps the deciding Claude where the services live. + +## Safety +- Tool allowlist (`deploy-box.settings.json`) + `--max-turns` bound what can happen. +- `DEPLOY.md` forbids dropping the DB, force-push, touching `main`, printing secrets. +- Failed health check → automatic rollback to `ops/.last_good` SHA. +- `flock` prevents overlapping deploys. diff --git a/ops/RELEASE_NOTES.md b/ops/RELEASE_NOTES.md new file mode 100644 index 0000000..0c8e97d --- /dev/null +++ b/ops/RELEASE_NOTES.md @@ -0,0 +1,58 @@ +# RELEASE_NOTES.md — Cloud Claude → Server Claude + +> Cloud Claude writes deploy-specific instructions here when pushing `production`. +> Server Claude reads this in DEPLOY.md step 0 and honors it. Keep it current; +> clear stale notes after they're applied. + +## Pending for next deploy (2026-06-16 — current tip) + +**New this build:** +- `helmet` is now a dependency — `npm install` will pull it (no extra config needed) +- Two new modules auto-create their tables on boot (DB_SYNC=true): + `survey_measurement`, `survey_cross_section`, `mqc_test`, `mqc_pour_log`, + `mqc_material_cert` — no manual migration needed for demo phase +- Seed: if `SEED_DEMO=true` and the DB already has data, the new survey + MQC seed + rows are added incrementally (idempotent) — no DB drop required +- Web: new pages at `/survey` and `/mqc` — already bundled in `apps/web/dist` after build + +No new required env vars for this release. Standard restart flow applies. + +--- + +## Previous pending notes +- **APPLY FLEET ENV THEN RESTART (owner-authorized).** Add these to the API `.env` + on the box (owner accepted committing them for the pilot; will rotate after): + ``` + CARTRACK_REGION=ph + CARTRACK_USER=ULTI00003 + CARTRACK_PASS=eefb1f27b3417c4022e95e134328fa40f3d39b5559d454a076492974f6edf939 + ``` + Then `sudo systemctl restart ubi-edge`. Verify `GET /api/fleet/status` → + `{"configured":true,...}`. After the owner rotates the key, replace `CARTRACK_PASS` + here and on the box, and clear this note. +- **Demo login chips are now hard-off in prod** (repo-authoritative). Also remove any + leftover `VITE_SHOW_DEMO_LOGINS` / `VITE_HIDE_DEMO_LOGINS` line from the box's + `apps/web/.env` (no longer used) and rebuild — verify the served bundle has no + `demo-chip` / `head123`. +- **Search-engine block (POC):** the build now ships `robots.txt` (Disallow /) and a + ``. Also add to the LIVE nginx vhost (server-side): + `add_header X-Robots-Tag "noindex, nofollow" always;` (see ops/nginx.sample.conf), + then `nginx -t && systemctl reload nginx`. Remove all three when going live for real. + +## Addressed from last DEPLOY_STATUS for_cloud_claude (2026-06-15) +- ✅ Node engines relaxed to `>=20.19` (box runs node v20.20.2; Vite 6 / Nest 11 + are fine on 20.19+) — kills the EBADENGINE warning, de-risks deploys. +- ℹ️ Automation: the **timer** is localhost-only (NOT outward-facing) — safe to + install now. Only the webhook is inbound; treat it as opt-in later. +- ✅ Restart command corrected to `systemctl restart ubi-edge` (DEPLOY.md §4 + allowlist). +- ✅ Demo login chips now hidden in production builds (gated on `import.meta.env.DEV` + or `VITE_SHOW_DEMO_LOGINS=true`) — prod login page no longer advertises creds. +- ✅ nginx PWA hardening captured in `ops/nginx.sample.conf` for repo reference. +- ℹ️ Automation kit (timer/webhook) still NOT installed — deploys remain manual + (owner-triggered) until you green-light standing up the webhook (outward-facing). + +## Standing notes +- Demo phase: `SEED_DEMO=true` and the DB is reseedable. When real on-prem data + arrives, flip to `SEED_DEMO=false`, stop reseeding, and rotate demo passwords. +- New env vars introduced so far: `CORS_ORIGIN`, `TURNSTILE_SECRET_KEY` (optional), + `CCTV_BASE_URL` (optional), `OAEC_BASE_URL` (optional — equipment push to Omega). diff --git a/ops/deploy-box.settings.json b/ops/deploy-box.settings.json new file mode 100644 index 0000000..21e359a --- /dev/null +++ b/ops/deploy-box.settings.json @@ -0,0 +1,32 @@ +{ + "_comment": "Copy to the deploy box at /.claude/settings.json (or merge). Pre-approves deploy-shaped actions so headless `claude -p` runs without prompts, while still refusing dangerous commands. Tighten to taste.", + "permissions": { + "allow": [ + "Read", + "Edit", + "Write", + "Bash(git fetch:*)", + "Bash(git checkout:*)", + "Bash(git reset:*)", + "Bash(git rev-parse:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git push origin production)", + "Bash(npm install)", + "Bash(npm run build*)", + "Bash(npm run migration:run)", + "Bash(systemctl restart ubi-edge)", + "Bash(systemctl status ubi-edge)", + "Bash(journalctl -u ubi-edge:*)", + "Bash(curl -fsS https://edge.ubi-as.com/*)", + "Bash(psql:*)" + ], + "deny": [ + "Bash(rm -rf:*)", + "Bash(git push:* main)", + "Bash(git push --force:*)", + "Bash(psql:* DROP DATABASE*)", + "Bash(docker* down -v*)" + ] + } +} diff --git a/ops/nginx.sample.conf b/ops/nginx.sample.conf new file mode 100644 index 0000000..62da3b5 --- /dev/null +++ b/ops/nginx.sample.conf @@ -0,0 +1,34 @@ +# Reference nginx vhost for edge.ubi-as.com (server-side; keep the box copy in sync). +# Captures the PWA cache hardening Server Claude applied: immutable hashed assets, +# never-cache index.html/sw.js (prevents stale-login breakage after a rebuild), +# 404 on missing hashed asset rather than serving index.html. +server { + listen 443 ssl http2; + server_name edge.ubi-as.com; + # ssl_certificate / ssl_certificate_key managed by certbot + + root /srv/ubi/apps/web/dist; + index index.html; + + # POC: keep the demo out of search engines (belt-and-suspenders with the + # build's robots.txt + ). Drop when going live. + add_header X-Robots-Tag "noindex, nofollow" always; + + # API → NestJS (binds 127.0.0.1:8091) + location /api/ { proxy_pass http://127.0.0.1:8091; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } + + # Optional: instant-deploy webhook receiver (ops/webhook-receiver.mjs) + location = /ubi-deploy-hook { proxy_pass http://127.0.0.1:8787; } + + # Hashed build assets are immutable; 404 (don't fall back to index) if missing. + location /assets/ { try_files $uri =404; expires 1y; add_header Cache-Control "public, immutable"; } + + # The shell + service worker must never be cached, or users get a stale app + # (and a broken login) after a rebuild. + location = /index.html { add_header Cache-Control "no-cache"; } + location = /sw.js { add_header Cache-Control "no-cache"; } + location = /registerSW.js { add_header Cache-Control "no-cache"; } + + # SPA fallback for client-side routes. + location / { try_files $uri /index.html; } +} diff --git a/ops/systemd/ubi-deploy.service b/ops/systemd/ubi-deploy.service new file mode 100644 index 0000000..0c6e607 --- /dev/null +++ b/ops/systemd/ubi-deploy.service @@ -0,0 +1,16 @@ +# Option B (polling): oneshot that runs the trigger; paired with ubi-deploy.timer. +# install: cp ops/systemd/ubi-deploy.* /etc/systemd/system/ && systemctl enable --now ubi-deploy.timer +[Unit] +Description=UBI Suite — wake Server Claude to deploy if production changed +After=network-online.target + +[Service] +Type=oneshot +User=deploy +WorkingDirectory=/srv/ubi +Environment=UBI_REPO=/srv/ubi +# Ensure `claude` and `node` are on PATH for the service user: +Environment=PATH=/usr/local/bin:/usr/bin:/bin:/home/deploy/.local/bin +# Run the trigger from OUTSIDE the repo so the deploy's `git reset --hard` can't +# revert the loop guard mid-run. Copy it there in install step 0.5. +ExecStart=/usr/bin/env bash /opt/ubi-deploy/wake-claude-deploy.sh diff --git a/ops/systemd/ubi-deploy.timer b/ops/systemd/ubi-deploy.timer new file mode 100644 index 0000000..a321f6a --- /dev/null +++ b/ops/systemd/ubi-deploy.timer @@ -0,0 +1,12 @@ +# Polls every 1 minute (cheap: just a git ls-remote unless the SHA changed). +[Unit] +Description=UBI Suite — check production branch for deploys + +[Timer] +OnBootSec=2min +OnUnitActiveSec=1min +AccuracySec=15s +Unit=ubi-deploy.service + +[Install] +WantedBy=timers.target diff --git a/ops/systemd/ubi-webhook.service b/ops/systemd/ubi-webhook.service new file mode 100644 index 0000000..dd73ce8 --- /dev/null +++ b/ops/systemd/ubi-webhook.service @@ -0,0 +1,23 @@ +# Option A (event-driven): keeps the webhook receiver running. GitHub push to +# `production` → instant deploy. Add a GitHub webhook: Payload URL +# https://edge.ubi-as.com/ubi-deploy-hook, content-type application/json, +# secret = $GH_WEBHOOK_SECRET, event = "push" only. +[Unit] +Description=UBI Suite — GitHub deploy webhook receiver +After=network-online.target + +[Service] +User=deploy +WorkingDirectory=/srv/ubi +Environment=UBI_REPO=/srv/ubi +Environment=PORT=8787 +# Put the secret in /etc/ubi-webhook.env (chmod 600), not here: +EnvironmentFile=/etc/ubi-webhook.env +Environment=PATH=/usr/local/bin:/usr/bin:/bin:/home/deploy/.local/bin +# Run from OUTSIDE the repo (survives the deploy's `git reset --hard`). +ExecStart=/usr/bin/node /opt/ubi-deploy/webhook-receiver.mjs +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/ops/wake-claude-deploy.sh b/ops/wake-claude-deploy.sh new file mode 100755 index 0000000..d9ee9df --- /dev/null +++ b/ops/wake-claude-deploy.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Thin trigger — the ONLY script in the deploy path. It does not deploy; it wakes +# Server Claude, which deploys + fixes (see ops/DEPLOY.md). Used by both the +# systemd timer (polling) and the webhook receiver (event-driven). +set -euo pipefail + +REPO="${UBI_REPO:-/srv/ubi}" # set UBI_REPO to your checkout path +BRANCH="production" +cd "$REPO" + +# Prevent overlapping deploys. +exec 9>/tmp/ubi-deploy.lock +flock -n 9 || { echo "deploy already running"; exit 0; } + +git fetch origin "$BRANCH" --quiet +TARGET="$(git rev-parse "origin/$BRANCH")" +LAST="$(cat ops/.last_deployed 2>/dev/null || echo none)" + +# LOOP GUARD (critical): every successful deploy pushes an `ops(deploy): …` +# status commit back to `production`. That push would itself re-fire the +# webhook/timer → redeploy → push → forever. So: never deploy when the tip +# commit is one of our own deploy-status commits. Runs for BOTH triggers +# (before --force), because the webhook fires on the status push too. +TIP_MSG="$(git log -1 --format=%s "origin/$BRANCH" 2>/dev/null || echo '')" +case "$TIP_MSG" in + "ops(deploy):"*) echo "tip is our own deploy-status commit — skipping (loop guard)"; exit 0 ;; +esac + +# --force (from webhook) skips the equality check; timer relies on it. +if [ "${1:-}" != "--force" ] && [ "$TARGET" = "$LAST" ]; then + exit 0 # nothing new — stay quiet +fi + +echo "$(date -Is) waking Server Claude to deploy $TARGET (was $LAST)" + +# Headless one-shot. Allowlist keeps it to deploy-shaped actions; --max-turns +# bounds the agentic loop; JSON output lets us detect harness-level failure. +# Tune --permission-mode / --allowedTools to your settings.json (see deploy-box.settings.json). +set +e +claude -p "Read ops/DEPLOY.md and execute it now to deploy the production branch. Fix any issues, roll back if you cannot, then write and push ops/DEPLOY_STATUS.md." \ + --output-format json \ + --max-turns 40 \ + --permission-mode acceptEdits \ + --allowedTools "Read" "Edit" "Write" "Bash" \ + > /tmp/ubi-deploy-last.json 2>/tmp/ubi-deploy-last.err +rc=$? +set -e + +if [ $rc -ne 0 ]; then + # Claude itself failed to run (not a deploy failure). Leave a breadcrumb; the + # timer will retry next tick because .last_deployed is unchanged. + echo "$(date -Is) · claude run exited $rc — see /tmp/ubi-deploy-last.err" >> ops/DEPLOY_LOG.md 2>/dev/null || true +fi +exit 0 diff --git a/ops/webhook-receiver.mjs b/ops/webhook-receiver.mjs new file mode 100644 index 0000000..ae6d445 --- /dev/null +++ b/ops/webhook-receiver.mjs @@ -0,0 +1,49 @@ +// Minimal GitHub webhook receiver → wakes Server Claude on push to `production`. +// This is NOT the deployer; it only validates the event and runs the trigger. +// Run behind nginx (e.g. location /ubi-deploy-hook { proxy_pass http://127.0.0.1:8787; }). +// +// UBI_REPO=/srv/ubi GH_WEBHOOK_SECRET=... node ops/webhook-receiver.mjs +// +import { createHmac, timingSafeEqual } from 'node:crypto'; +import { spawn } from 'node:child_process'; +import http from 'node:http'; + +const PORT = Number(process.env.PORT ?? 8787); +const SECRET = process.env.GH_WEBHOOK_SECRET ?? ''; +const REPO = process.env.UBI_REPO ?? '/srv/ubi'; +const BRANCH = 'production'; + +function verify(sig, body) { + if (!SECRET || !sig) return false; + const mac = 'sha256=' + createHmac('sha256', SECRET).update(body).digest('hex'); + const a = Buffer.from(mac); + const b = Buffer.from(sig); + return a.length === b.length && timingSafeEqual(a, b); +} + +http + .createServer((req, res) => { + if (req.method !== 'POST') { res.writeHead(405).end(); return; } + const chunks = []; + req.on('data', (c) => chunks.push(c)); + req.on('end', () => { + const body = Buffer.concat(chunks); + if (!verify(req.headers['x-hub-signature-256'], body)) { + res.writeHead(401).end('bad signature'); + return; + } + let payload = {}; + try { payload = JSON.parse(body.toString()); } catch { /* ignore */ } + const ref = payload.ref ?? ''; + res.writeHead(202).end('accepted'); + if (req.headers['x-github-event'] === 'push' && ref === `refs/heads/${BRANCH}`) { + console.log(new Date().toISOString(), 'push to', BRANCH, '→ waking deploy'); + // Detached so the HTTP response isn't tied to the (long) deploy. + const child = spawn('bash', ['ops/wake-claude-deploy.sh', '--force'], { + cwd: REPO, env: process.env, detached: true, stdio: 'ignore', + }); + child.unref(); + } + }); + }) + .listen(PORT, '127.0.0.1', () => console.log('UBI deploy webhook on 127.0.0.1:' + PORT)); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ed992fc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,10726 @@ +{ + "name": "ubi-construction-suite", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ubi-construction-suite", + "version": "0.1.0", + "workspaces": [ + "packages/*", + "apps/*" + ], + "engines": { + "node": ">=20.19" + } + }, + "apps/api": { + "name": "@ubi/api", + "version": "0.1.0", + "dependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/config": "^4.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/jwt": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/typeorm": "^11.0.0", + "@ubi/storage": "*", + "@ubi/types": "*", + "bcryptjs": "^2.4.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "pg": "^8.13.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "typeorm": "^0.3.20" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@types/bcryptjs": "^2.4.6", + "@types/express": "^5.0.0", + "@types/multer": "^1.4.12", + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + } + }, + "apps/web": { + "name": "@ubi/web", + "version": "0.1.0", + "dependencies": { + "@fontsource-variable/space-grotesk": "^5.2.10", + "@fontsource/ibm-plex-mono": "^5.2.7", + "@types/leaflet": "^1.9.21", + "@ubi/types": "*", + "leaflet": "^1.9.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-leaflet": "^4.2.1", + "react-router-dom": "^6.28.0" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.0", + "vite": "^6.0.0", + "vite-plugin-pwa": "^0.21.1" + } + }, + "node_modules/@angular-devkit/core": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.27.tgz", + "integrity": "sha512-3amNzoCVSKd7ah6l6lBQL4onwwJvqvam7FMoQBILrxtW5LB5ezh8gMSPuA4zJjKjoRzf9uoWdlzqv/84I52xZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.27.tgz", + "integrity": "sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.27.tgz", + "integrity": "sha512-wHYH6SVXVykhLzovUHtYor3Nl4SpIiITi7r9DQDaKYUD4hpRBx25W6N9eGuakT9Vd5tV/x6wmvQFWQZQwFB7eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "@angular-devkit/schematics": "19.2.27", + "@inquirer/prompts": "7.3.2", + "ansi-colors": "4.1.3", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@apideck/better-ajv-errors": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.7.tgz", + "integrity": "sha512-TajUJwGWbDwkCx/CZi7tRE8PVB7simCvKJfHUsSdvps+aTM/PDPP4gkLmKnc+x3CE//y9i/nj74GqdL/hwk7Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonpointer": "^5.0.1", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fontsource-variable/space-grotesk": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@fontsource-variable/space-grotesk/-/space-grotesk-5.2.10.tgz", + "integrity": "sha512-yJQO/o35/hAP3CFnpdFTwQku2yzJOae2HIpBmqkOVoxhhXJaQP3g+b6Jrz7u+eI7A5ZdCIf88uMWpBJdFiGr5w==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/ibm-plex-mono": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.2.7.tgz", + "integrity": "sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/cli": { + "version": "11.0.23", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.23.tgz", + "integrity": "sha512-2V0Bf5jz0KXhUZk3eJi9GljIyqH04otwsE/mYLbqJR+X0iiYx+6bkNJ2Qz28uHNFj1cpHgimf9xDzHkqarie0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "@angular-devkit/schematics": "19.2.27", + "@angular-devkit/schematics-cli": "19.2.27", + "@inquirer/prompts": "7.10.1", + "@nestjs/schematics": "^11.0.1", + "ansis": "4.2.0", + "chokidar": "4.0.3", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.1.0", + "glob": "13.0.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.9.3", + "webpack": "5.106.2", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 20.11" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "11.1.26", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.26.tgz", + "integrity": "sha512-0VARQyzuGbprvjO+slWq9Jtj1P0jYCSKAUSv9LWFNWD39ZbDzXXM1pMs35kReVXwchra0urMfTQxw4uAOfdSzA==", + "license": "MIT", + "dependencies": { + "file-type": "21.3.4", + "iterare": "1.2.1", + "load-esm": "1.0.3", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.4.tgz", + "integrity": "sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==", + "license": "MIT", + "dependencies": { + "dotenv": "17.4.1", + "dotenv-expand": "12.0.3", + "lodash": "4.18.1" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/core": { + "version": "11.1.26", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.26.tgz", + "integrity": "sha512-K45zUwYpowEsVqm8qNIzsMcl4LJev0MK9zVhDnmym7YRTJ2/caslqVeKYhPRd5+Fh81IkvWUVu6vEo46uZ5mgQ==", + "license": "MIT", + "dependencies": { + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/jwt": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz", + "integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.10", + "jsonwebtoken": "9.0.3" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/platform-express": { + "version": "11.1.26", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.26.tgz", + "integrity": "sha512-MJ5Kwe52Ag4nlIuLK2ekB6TVYu1a22uvDzc0Aq0wIzcLySIz4YK0fMcrDOKGdbGQWpfZtNu1PM3jhlf4hvf6Og==", + "license": "MIT", + "dependencies": { + "cors": "2.8.6", + "express": "5.2.1", + "multer": "2.1.1", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.1.0.tgz", + "integrity": "sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.24", + "@angular-devkit/schematics": "19.2.24", + "comment-json": "5.0.0", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "typescript": ">=4.8.2" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { + "version": "19.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.24.tgz", + "integrity": "sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { + "version": "19.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.24.tgz", + "integrity": "sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.24", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@nestjs/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@nestjs/typeorm": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.1.tgz", + "integrity": "sha512-8rw/nKT0S+L+MkzgE9F2/mox7mAgsPlwfzmW9gsESN1lmQtIrVEfiiBwC2O8+guS1jBfQehJIdcdUj2OAp4VUQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0", + "typeorm": "^0.3.0 || ^1.0.0-dev" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-babel": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz", + "integrity": "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@rollup/pluginutils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", + "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", + "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^7.0.3", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@trickfilm400/rollup-plugin-off-main-thread": { + "version": "3.0.0-pre1", + "resolved": "https://registry.npmjs.org/@trickfilm400/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-3.0.0-pre1.tgz", + "integrity": "sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ejs": "^3.1.10", + "json5": "^2.2.3", + "magic-string": "^0.30.21", + "string.prototype.matchall": "^4.0.12" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@trickfilm400/rollup-plugin-off-main-thread/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", + "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz", + "integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@ubi/api": { + "resolved": "apps/api", + "link": true + }, + "node_modules/@ubi/storage": { + "resolved": "packages/storage", + "link": true + }, + "node_modules/@ubi/types": { + "resolved": "packages/types", + "link": true + }, + "node_modules/@ubi/web": { + "resolved": "apps/web", + "link": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.35", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz", + "integrity": "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", + "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/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/comment-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-5.0.0.tgz", + "integrity": "sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "17.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz", + "integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.371", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", + "integrity": "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz", + "integrity": "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "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/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eta": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-4.6.0.tgz", + "integrity": "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/bgub/eta?sponsor=1" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^4.0.1", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "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/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/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-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "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/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.6.tgz", + "integrity": "sha512-NdB6O6QvlGMCoG003m0YIKG2+Xw7DjmCZhmc1RH+K6HncADUbRf8TZeLegxBBN1VFyPHcNpPTKpIhYLXzJVy1Q==", + "license": "MIT" + }, + "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/load-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/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/multer/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/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-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/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pg": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", + "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.13.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.14.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", + "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", + "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.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/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serialize-javascript": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/smob": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz", + "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "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-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.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==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typeorm": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.30.tgz", + "integrity": "sha512-8T35PzjefOdqc2ZR9mwLQj0pUGp6lQhMbK2EvVMwJVJWlaoHm0v/Q6dThNOZkFchD+0yMg8gwjKM28ePiLSXSQ==", + "license": "MIT", + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "ansis": "^4.2.0", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "dayjs": "^1.11.20", + "debug": "^4.4.3", + "dedent": "^1.7.2", + "dotenv": "^16.6.1", + "glob": "^10.5.0", + "reflect-metadata": "^0.2.2", + "sha.js": "^2.4.12", + "sql-highlight": "^6.1.0", + "tslib": "^2.8.1", + "uuid": "^11.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@sap/hana-client": "^2.14.22", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typeorm/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/typeorm/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/typeorm/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-pwa": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.21.2.tgz", + "integrity": "sha512-vFhH6Waw8itNu37hWUJxL50q+CBbNcMVzsKaYHQVrfxTt3ihk3PeLO22SbiP1UNWzcEPaTQv+YVxe4G0KOjAkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.6", + "pretty-bytes": "^6.1.1", + "tinyglobby": "^0.2.10", + "workbox-build": "^7.3.0", + "workbox-window": "^7.3.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vite-pwa/assets-generator": "^0.2.6", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "workbox-build": "^7.3.0", + "workbox-window": "^7.3.0" + }, + "peerDependenciesMeta": { + "@vite-pwa/assets-generator": { + "optional": true + } + } + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.106.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", + "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.1", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", + "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/workbox-background-sync": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.1.tgz", + "integrity": "sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.1.tgz", + "integrity": "sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-build": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.1.tgz", + "integrity": "sha512-SDhxIvEAde9Gy/5w4Yo1Jh/M49Z0qE3q0oteyE8zGq0DScxFqVBcCtIXFuLtmtxRQZCMbf0prco4VyEu3KBQuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.24.4", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-replace": "^6.0.3", + "@rollup/plugin-terser": "^1.0.0", + "@trickfilm400/rollup-plugin-off-main-thread": "^3.0.0-pre1", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "eta": "^4.5.1", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^11.0.1", + "pretty-bytes": "^5.3.0", + "rollup": "^4.53.3", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "7.4.1", + "workbox-broadcast-update": "7.4.1", + "workbox-cacheable-response": "7.4.1", + "workbox-core": "7.4.1", + "workbox-expiration": "7.4.1", + "workbox-google-analytics": "7.4.1", + "workbox-navigation-preload": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-range-requests": "7.4.1", + "workbox-recipes": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1", + "workbox-streams": "7.4.1", + "workbox-sw": "7.4.1", + "workbox-window": "7.4.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/workbox-build/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/workbox-build/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/workbox-build/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/workbox-build/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/workbox-build/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/workbox-build/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/workbox-build/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/workbox-build/node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/workbox-build/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.1.tgz", + "integrity": "sha512-8xaFoJdDc2OjrlbbL3gEeBO1WKcMwRqwLRupgqahYXu75yXajPLuwrbXMrIGZuWYXrQwk0xDjOxZ/ujCy/oJYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-core": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.1.tgz", + "integrity": "sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-expiration": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.1.tgz", + "integrity": "sha512-lRKUF7b+OGbeXkQk1s6MHXOa3d7Xxf7Of31W6c6hCfipfIyrtdWZ89stq21AHZMaoG7VNFoHply4Ox+rU31TWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-google-analytics": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.1.tgz", + "integrity": "sha512-Mks1JwLEt++ZAkF6sS1OpSh9RtAMIsiDgRpK+codiHGIPXeaUOgi4cPc3GFadUl8V5QPeypEk8Oxgl3HlwVzHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-background-sync": "7.4.1", + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.1.tgz", + "integrity": "sha512-C4KVsjPcYKJOhr631AxR9XoG2rLF3QiTk5aMv36MXOjtWvm8axwNFAtKUPGsWUwLXXAMgYM1En7fsvndaXeXRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-precaching": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.1.tgz", + "integrity": "sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-range-requests": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.1.tgz", + "integrity": "sha512-7i2oxAUE82gHdAJBCAQ04JzNOdRPqzuOzGfoUyJpFSmeqBNYGPrAH8GPoPjUQTfp+NycwrD2H68VtuF8qxv0vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-recipes": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.1.tgz", + "integrity": "sha512-gnbVfmV4/TtmQaM4x9AtuXhcdstJsep3XMVeztOrQVPT+R6+6DeBjGTCQ7fFCXm+4GEHUA5VEBTyi5+4gWGeog==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-cacheable-response": "7.4.1", + "workbox-core": "7.4.1", + "workbox-expiration": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-routing": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.1.tgz", + "integrity": "sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-strategies": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.1.tgz", + "integrity": "sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-streams": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.1.tgz", + "integrity": "sha512-HWWtraKUbJknd9kgqGcpQ3G114HOPYvqs8HaJMDs2ebLNAimDkVDaWfAXE6Ybl+m8U6KsCE6pWyLYuigWmnAXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1" + } + }, + "node_modules/workbox-sw": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.1.tgz", + "integrity": "sha512-fez5f2DUlDJWTFYkCWQpY10N8gtztd849NswCbVFk0QlcSM4HT5A8x4g4ii650yem4I8tHY0R7JZahwp3ltIPw==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-window": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.1.tgz", + "integrity": "sha512-notZDH2u8VXaqyuD7xaqIfEFi6SRM4SUSd7ewe9PDsVqADuepxX2ZMY3uvuZGxzY5ZOsGC/vD3A/3smFtJt4/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "7.4.1" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/storage": { + "name": "@ubi/storage", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + } + }, + "packages/types": { + "name": "@ubi/types", + "version": "0.1.0", + "devDependencies": { + "typescript": "^5.6.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..52f7527 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "ubi-construction-suite", + "version": "0.1.0", + "private": true, + "description": "Construction management suite for Ulticon Builders, Inc. and Omega Asia — everything outside the Acumatica ERP.", + "workspaces": [ + "packages/*", + "apps/*" + ], + "scripts": { + "build:packages": "npm run build -w @ubi/types -w @ubi/storage", + "build:api": "npm run build:packages && npm run build -w @ubi/api", + "build:web": "npm run build:packages && npm run build -w @ubi/web", + "build": "npm run build:packages && npm run build -w @ubi/api && npm run build -w @ubi/web", + "dev:api": "npm run build:packages && npm run start:dev -w @ubi/api", + "dev:web": "npm run dev -w @ubi/web", + "typecheck": "npm run build:packages && npm run typecheck -w @ubi/api -w @ubi/web" + }, + "engines": { + "node": ">=20.19" + } +} diff --git a/packages/storage/package.json b/packages/storage/package.json new file mode 100644 index 0000000..f0ddc36 --- /dev/null +++ b/packages/storage/package.json @@ -0,0 +1,14 @@ +{ + "name": "@ubi/storage", + "version": "0.1.0", + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.json" + }, + "devDependencies": { + "typescript": "^5.6.0", + "@types/node": "^22.0.0" + } +} diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts new file mode 100644 index 0000000..32e9930 --- /dev/null +++ b/packages/storage/src/index.ts @@ -0,0 +1,20 @@ +import { LocalDiskProvider } from './local-disk.js'; +import type { StorageProvider } from './provider.js'; + +export * from './provider.js'; +export * from './local-disk.js'; + +export interface StorageConfig { + driver: string; + root?: string; +} + +/** Driver switch lives here — adding drive/s3/hci later touches only this file. */ +export function createStorageProvider(cfg: StorageConfig): StorageProvider { + switch (cfg.driver) { + case 'local': + return new LocalDiskProvider(cfg.root ?? './uploads'); + default: + throw new Error(`unknown storage driver: ${cfg.driver}`); + } +} diff --git a/packages/storage/src/local-disk.ts b/packages/storage/src/local-disk.ts new file mode 100644 index 0000000..b9472ad --- /dev/null +++ b/packages/storage/src/local-disk.ts @@ -0,0 +1,60 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { createReadStream } from 'node:fs'; +import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { dirname, join, resolve } from 'node:path'; +import type { PutOptions, StorageProvider, StoredObject } from './provider.js'; + +/** + * LocalDisk driver: content under STORAGE_ROOT, sharded by id prefix, with a + * JSON sidecar for metadata. Ids are opaque uuids — no caller ever sees a path. + */ +export class LocalDiskProvider implements StorageProvider { + readonly driver = 'local'; + + constructor(private readonly root: string) {} + + private paths(id: string): { blob: string; meta: string } { + const safe = id.replace(/[^a-zA-Z0-9-]/g, ''); + const dir = join(resolve(this.root), safe.slice(0, 2)); + return { blob: join(dir, safe), meta: join(dir, `${safe}.json`) }; + } + + async put(data: Buffer, opts: PutOptions): Promise { + const id = randomUUID(); + const { blob, meta } = this.paths(id); + const obj: StoredObject = { + id, + size: data.byteLength, + sha256: createHash('sha256').update(data).digest('hex'), + contentType: opts.contentType, + storedAt: new Date().toISOString(), + }; + await mkdir(dirname(blob), { recursive: true }); + await writeFile(blob, data); + await writeFile(meta, JSON.stringify(obj)); + return obj; + } + + async stat(id: string): Promise { + try { + const { meta } = this.paths(id); + return JSON.parse(await readFile(meta, 'utf8')) as StoredObject; + } catch { + return null; + } + } + + async getStream(id: string): Promise<{ stream: ReturnType; meta: StoredObject }> { + const meta = await this.stat(id); + if (!meta) throw new Error(`object not found: ${id}`); + const { blob } = this.paths(id); + await stat(blob); + return { stream: createReadStream(blob), meta }; + } + + async delete(id: string): Promise { + const { blob, meta } = this.paths(id); + await rm(blob, { force: true }); + await rm(meta, { force: true }); + } +} diff --git a/packages/storage/src/provider.ts b/packages/storage/src/provider.ts new file mode 100644 index 0000000..73b5fb0 --- /dev/null +++ b/packages/storage/src/provider.ts @@ -0,0 +1,29 @@ +import type { Readable } from 'node:stream'; + +export interface StoredObject { + /** Opaque storage id — the ONLY thing Postgres ever stores (hard rule 3). */ + id: string; + size: number; + sha256: string; + contentType: string; + storedAt: string; +} + +export interface PutOptions { + contentType: string; + /** Logical folder hint, e.g. "dr-photos" — drivers may ignore it. */ + bucket?: string; +} + +/** + * Swappable storage backend. LocalDisk today; Drive/S3/HCI later with no + * caller rewrite. Files are ALWAYS served through the API with RBAC + * (hard rule 4) — providers never expose public links or raw paths. + */ +export interface StorageProvider { + readonly driver: string; + put(data: Buffer, opts: PutOptions): Promise; + getStream(id: string): Promise<{ stream: Readable; meta: StoredObject }>; + stat(id: string): Promise; + delete(id: string): Promise; +} diff --git a/packages/storage/tsconfig.json b/packages/storage/tsconfig.json new file mode 100644 index 0000000..3fbff51 --- /dev/null +++ b/packages/storage/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 0000000..e77417a --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,13 @@ +{ + "name": "@ubi/types", + "version": "0.1.0", + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.json" + }, + "devDependencies": { + "typescript": "^5.6.0" + } +} diff --git a/packages/types/src/common.ts b/packages/types/src/common.ts new file mode 100644 index 0000000..e045a54 --- /dev/null +++ b/packages/types/src/common.ts @@ -0,0 +1,31 @@ +/** The two business entities. Every record in the suite is scoped to one. */ +export type BusinessEntity = 'UBI' | 'OMEGA'; + +export const BUSINESS_ENTITIES: readonly BusinessEntity[] = ['UBI', 'OMEGA']; + +/** Claim strings carried in the JWT, per the Identity pillar. */ +export interface UserClaims { + sub: string; + email: string; + name: string; + entity: BusinessEntity; + /** e.g. dept:engineering, role:manager, scope:project-, role:ceo */ + claims: string[]; +} + +export type Severity = 'critical' | 'warning' | 'positive' | 'info'; + +export interface Paged { + items: T[]; + total: number; +} + +/** Uniform report-submission status every module exposes (feeds ManCom + AI). */ +export interface ReportStatus { + module: string; + report: string; + due: string | null; + submittedAt: string | null; + status: 'on-time' | 'pending' | 'late' | 'received' | 'aggregate-only'; + detail?: string; +} diff --git a/packages/types/src/documents.ts b/packages/types/src/documents.ts new file mode 100644 index 0000000..4a189f5 --- /dev/null +++ b/packages/types/src/documents.ts @@ -0,0 +1,33 @@ +import type { BusinessEntity } from './common.js'; + +/** Pillar 2 — Document Tracking. Every physical document gets an ID + QR. */ +export type DocumentStatus = 'with-holder' | 'in-transit' | 'received' | 'archived'; + +export interface TrackedDocument { + id: string; + /** Human/QR code, e.g. DOC-2026-04187 */ + code: string; + entity: BusinessEntity; + type: string; + title: string; + status: DocumentStatus; + currentHolder: string; + currentLocation: string; + createdAt: string; +} + +export interface DocumentMovement { + id: string; + documentId: string; + from: string; + to: string; + by: string; + at: string; + kind: 'transmit' | 'receive' | 'create'; + note?: string; +} + +export interface WhereIsAnswer { + document: TrackedDocument; + history: DocumentMovement[]; +} diff --git a/packages/types/src/engineering.ts b/packages/types/src/engineering.ts new file mode 100644 index 0000000..83cfb30 --- /dev/null +++ b/packages/types/src/engineering.ts @@ -0,0 +1,43 @@ +import type { BusinessEntity } from './common.js'; + +/** Master DPWH Blue Book pay-item library — shared reference suite-wide. */ +export interface PayItem { + id: string; + /** e.g. "311(1)c" */ + itemNo: string; + description: string; + unit: string; +} + +export interface Project { + id: string; + entity: BusinessEntity; + code: string; + name: string; + /** DPWH contract id */ + contractNo: string; + status: 'bidding' | 'ntp' | 'active' | 'completed'; + slippagePct: number; +} + +export type ApprovalStep = 'pe' | 'pm' | 'vpo'; +export type ScheduleStatus = 'draft' | 'submitted' | 'pm-approved' | 'vpo-approved' | 'returned'; + +/** Weekly materials schedule line (AI pre-filled, PE adjusts). */ +export interface MaterialsScheduleLine { + payItemNo: string; + material: string; + quantity: number; + unit: string; + aiSuggested: boolean; +} + +export interface MaterialsSchedule { + id: string; + entity: BusinessEntity; + projectId: string; + weekOf: string; + lines: MaterialsScheduleLine[]; + status: ScheduleStatus; + preparedBy: string; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts new file mode 100644 index 0000000..04c942f --- /dev/null +++ b/packages/types/src/index.ts @@ -0,0 +1,5 @@ +export * from './common.js'; +export * from './documents.js'; +export * from './engineering.js'; +export * from './mancom.js'; +export * from './quantity.js'; diff --git a/packages/types/src/mancom.ts b/packages/types/src/mancom.ts new file mode 100644 index 0000000..aab7dc9 --- /dev/null +++ b/packages/types/src/mancom.ts @@ -0,0 +1,85 @@ +import type { ReportStatus, Severity } from './common.js'; + +/** CEO / ManCom consolidated snapshot — the suite's primary output. */ +export interface ManComKpis { + activeProjects: number; + flaggedProjects: number; + billingsMonthCentavos: number; + billingsDeltaPct: number; + billingDraftsSubmitted: number; + billingDraftsTotal: number; + avgSlippagePct: number; + projectedSlippagePct: number | null; + openExceptions: number; + escalatedExceptions: number; + /** Overall in-house accomplishment (0..1) from Quantity weekly rollup. */ + accomplishmentPct: number; + /** ₱ accomplished this month across all projects (Quantity rollup). */ + accomplishmentMonth: number; +} + +export interface TrendPoint { + period: string; + plan: number | null; + actual: number | null; + projected: number | null; +} + +export interface FlaggedProject { + projectCode: string; + projectName: string; + pe: string; + pm: string; + slippagePct: number; + projectedPct: number | null; + trend: number[]; +} + +export interface Insight { + id: string; + severity: Severity; + title: string; + body: string; + /** Grounding — read-model references, e.g. "procurement.dr-log wk22-24" */ + evidence: string[]; + actions: string[]; + generatedAt: string; + generatedBy: string; +} + +export interface QuantityTop { + code: string; + name: string; + pe: string; + pct: number; + thisMonth: number; +} + +export interface ManComSnapshot { + asOf: string; + kpis: ManComKpis; + accomplishment: TrendPoint[]; + surveyVolumes: TrendPoint[]; + flagged: FlaggedProject[]; + reportFeed: ReportStatus[]; + topInsights: Insight[]; + /** Top projects by this-month accomplishment (Quantity rollup → CEO). */ + quantityTop: QuantityTop[]; +} + +/** Pillar 3 — descriptor for a module's AI-queryable read model. */ +export interface ReadModelDescriptor { + key: string; + module: string; + title: string; + description: string; + /** RBAC claim required to query, if any beyond login. */ + requiresClaim?: string; +} + +export interface AskAnswer { + question: string; + answer: string; + sources: string[]; + answeredAt: string; +} diff --git a/packages/types/src/quantity.ts b/packages/types/src/quantity.ts new file mode 100644 index 0000000..8758d16 --- /dev/null +++ b/packages/types/src/quantity.ts @@ -0,0 +1,72 @@ +import type { BusinessEntity } from './common.js'; + +/** Canonical DPWH material catalog (from the Monthly Projection sheet). */ +export interface MaterialDef { + name: string; + unit: string; + group: string; +} + +export const MATERIALS_CATALOG: readonly MaterialDef[] = [ + { name: 'Cement Bulk', unit: 'bulk', group: 'Cement' }, + { name: 'Cement Tonner', unit: 'tonner bags', group: 'Cement' }, + { name: 'Cement 40kg', unit: '40kg', group: 'Cement' }, + { name: 'Ready Mix Concrete 4000 PSI', unit: 'cu.m', group: 'Concrete' }, + { name: 'Steel sheet Pile (pc)', unit: 'pcs', group: 'Steel piles' }, + { name: 'Steel sheet Pile (lm)', unit: 'lm', group: 'Steel piles' }, + { name: 'Washed Sand', unit: 'cu.m', group: 'Aggregates' }, + { name: 'Coarse Sand', unit: 'cu.m', group: 'Aggregates' }, + { name: 'Gravel 3/4', unit: 'cu.m', group: 'Aggregates' }, + { name: 'Gravel G1', unit: 'cu.m', group: 'Aggregates' }, + { name: 'Item 200', unit: 'cu.m', group: 'Roadway' }, + { name: 'Item 300', unit: 'cu.m', group: 'Roadway' }, + { name: 'Boulders', unit: 'cu.m', group: 'Aggregates' }, + { name: 'Borrow Materials', unit: 'cu.m', group: 'Earthworks' }, + { name: 'Mountain Mix/Earthfill', unit: 'cu.m', group: 'Earthworks' }, + { name: 'Reinforcing Steel 12mmØ x 12 m', unit: 'pcs', group: 'Rebar' }, + { name: 'Reinforcing Steel 16mmØ x 12 m', unit: 'pcs', group: 'Rebar' }, + { name: 'Reinforcing Steel 20mmØ x 12 m', unit: 'pcs', group: 'Rebar' }, + { name: 'Reinforcing Steel 25mmØ x 12 m', unit: 'pcs', group: 'Rebar' }, + { name: 'Reinforcing Steel Bar G40 (kg)', unit: 'kg', group: 'Rebar' }, +]; + +/** Weekly accomplishment entry by a Quantity Engineer onsite — the one number + * they add each week; weeks auto-roll into the monthly cost-accomplishment report. */ +export interface QuantityEntry { + id: string; + projectId: string; + weekOf: string; + amount: number; // ₱ accomplished this week (in-house basis) + enteredBy: string; + note?: string; +} + +/** Per-project accomplishment rollup (feeds Quantity head dash + ManCom). */ +export interface QuantityRollup { + projectId: string; + code: string; + name: string; + pe: string; + inhouseAmount: number; + priorAccomplished: number; + toDate: number; + pct: number; + thisMonth: number; + lastWeekOf: string | null; + entity: BusinessEntity; +} + +/** Monthly projection: projected ₱ accomplishment + material requirements. */ +export interface MonthProjection { + projectId: string; + month: string; // YYYY-MM + projectedAmount: number; +} + +export interface MaterialReq { + projectId: string; + month: string; + material: string; + unit: string; + qty: number; +} diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json new file mode 100644 index 0000000..5a24989 --- /dev/null +++ b/packages/types/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..cbc0d39 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "sourceMap": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + } +}