From 936533a9fb36e4f9a1faac2eb9245780ffe520ee Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 16:03:59 +0000 Subject: [PATCH 01/68] chore(kickoff): add context pack (CLAUDE.md, GIT.md, diagrams, ignores) Seed the repo with the kickoff context: master context (stack, hard rules, three pillars, 14 module specs, git strategy), branching quick reference, functional & DFD diagram set, and Node/TS monorepo ignores. https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- .gitignore | 20 ++ CLAUDE.md | 721 ++++++++++++++++++++++++++++++++++++++++++++++++ GIT.md | 34 +++ README.md | 35 ++- diagrams.html | 743 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1552 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 GIT.md create mode 100644 diagrams.html 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/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b315eae --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,721 @@ +# 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. + +--- + +## 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..8b857ed 100644 --- a/README.md +++ b/README.md @@ -1 +1,34 @@ -# 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 +Pre-code. Next: monorepo scaffold (API + PWA + Postgres + StorageProvider + +the three pillars stubbed), built on the module branches per the phasing. + +## 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/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. +
+ +
+ + From 7679a80aa2a53b220f061efb049ee565b826a6f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 16:03:59 +0000 Subject: [PATCH 02/68] feat(ceo/mancom): CEO module UI mockup set (mockups.html) Annotated design frames for the suite's primary output, per scope decision (CEO deep-dive first, brand light UI, static annotated frames): - ManCom live dashboard: KPI band, accomplishment-vs-plan and survey volume charts, flagged projects, department report feed, AI rail - Trend-projection toggle shown OFF vs ON (solid=actual, dashed= projected, grey=plan convention) - AI Insights register: grounded, severity-ranked, actions route into VPO inbox / Audit exceptions - Ask AI conversational view with per-claim source citations - Agentic-view sandboxed host shell (host contract visible) - Mobile ManCom (PWA) - Widget -> Pillar-3 read-model contract table Verified in headless Chromium (Playwright): no console errors, no horizontal overflow at 1280px. https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- mockups.html | 791 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 791 insertions(+) create mode 100644 mockups.html 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. +
+ +
+ + From 5601ccaaca5382c2bf6e7db50f37b376f06d322d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 16:03:59 +0000 Subject: [PATCH 03/68] =?UTF-8?q?chore(repo):=20monorepo=20scaffold=20?= =?UTF-8?q?=E2=80=94=20workspaces,=20strict=20tsconfig,=20env=20example,?= =?UTF-8?q?=20dev=20compose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- .env.example | 33 +++++++++++++++++++++++++++++++++ docker-compose.dev.yml | 21 +++++++++++++++++++++ package.json | 22 ++++++++++++++++++++++ tsconfig.base.json | 18 ++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 .env.example create mode 100644 docker-compose.dev.yml create mode 100644 package.json create mode 100644 tsconfig.base.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2dab9ef --- /dev/null +++ b/.env.example @@ -0,0 +1,33 @@ +# ── API ────────────────────────────────────────────────────────────── +PORT=3000 +JWT_SECRET=change-me +JWT_EXPIRES=8h +# 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= + +# ── External systems (integration stubs) ───────────────────────────── +ACUMATICA_BASE_URL= +OAEC_BASE_URL= 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/package.json b/package.json new file mode 100644 index 0000000..1181d7b --- /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": ">=22" + } +} 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 + } +} From ff249eaf1ff47d41e32536058f46ddd9356798f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 16:03:59 +0000 Subject: [PATCH 04/68] feat(packages): @ubi/types shared contracts + @ubi/storage StorageProvider with LocalDisk driver https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- packages/storage/package.json | 14 ++++++ packages/storage/src/index.ts | 20 +++++++++ packages/storage/src/local-disk.ts | 60 +++++++++++++++++++++++++ packages/storage/src/provider.ts | 29 ++++++++++++ packages/storage/tsconfig.json | 9 ++++ packages/types/package.json | 13 ++++++ packages/types/src/common.ts | 31 +++++++++++++ packages/types/src/documents.ts | 33 ++++++++++++++ packages/types/src/engineering.ts | 43 ++++++++++++++++++ packages/types/src/index.ts | 4 ++ packages/types/src/mancom.ts | 71 ++++++++++++++++++++++++++++++ packages/types/tsconfig.json | 8 ++++ 12 files changed, 335 insertions(+) create mode 100644 packages/storage/package.json create mode 100644 packages/storage/src/index.ts create mode 100644 packages/storage/src/local-disk.ts create mode 100644 packages/storage/src/provider.ts create mode 100644 packages/storage/tsconfig.json create mode 100644 packages/types/package.json create mode 100644 packages/types/src/common.ts create mode 100644 packages/types/src/documents.ts create mode 100644 packages/types/src/engineering.ts create mode 100644 packages/types/src/index.ts create mode 100644 packages/types/src/mancom.ts create mode 100644 packages/types/tsconfig.json 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..4206d19 --- /dev/null +++ b/packages/types/src/index.ts @@ -0,0 +1,4 @@ +export * from './common.js'; +export * from './documents.js'; +export * from './engineering.js'; +export * from './mancom.js'; diff --git a/packages/types/src/mancom.ts b/packages/types/src/mancom.ts new file mode 100644 index 0000000..0f35c98 --- /dev/null +++ b/packages/types/src/mancom.ts @@ -0,0 +1,71 @@ +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; +} + +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 ManComSnapshot { + asOf: string; + kpis: ManComKpis; + accomplishment: TrendPoint[]; + surveyVolumes: TrendPoint[]; + flagged: FlaggedProject[]; + reportFeed: ReportStatus[]; + topInsights: Insight[]; +} + +/** 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/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"] +} From 42971aaa3eefb8831c91010dacdb78673f366491 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 16:04:00 +0000 Subject: [PATCH 05/68] =?UTF-8?q?feat(api):=20NestJS=20bootstrap=20?= =?UTF-8?q?=E2=80=94=20strict=20config,=20RBAC=20guard=20+=20claims,=20ent?= =?UTF-8?q?ity-scoped=20base=20entity,=20health?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/nest-cli.json | 8 + apps/api/package.json | 36 + apps/api/src/common/base.entity.ts | 32 + apps/api/src/common/rbac.ts | 64 + apps/api/src/health/health.controller.ts | 11 + apps/api/src/main.ts | 17 + apps/api/tsconfig.json | 13 + package-lock.json | 5475 ++++++++++++++++++++++ 8 files changed, 5656 insertions(+) create mode 100644 apps/api/nest-cli.json create mode 100644 apps/api/package.json create mode 100644 apps/api/src/common/base.entity.ts create mode 100644 apps/api/src/common/rbac.ts create mode 100644 apps/api/src/health/health.controller.ts create mode 100644 apps/api/src/main.ts create mode 100644 apps/api/tsconfig.json create mode 100644 package-lock.json 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..5954bc4 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,36 @@ +{ + "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" + }, + "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" + } +} 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/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..2957c79 --- /dev/null +++ b/apps/api/src/main.ts @@ -0,0 +1,17 @@ +import 'reflect-metadata'; +import { ValidationPipe } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +async function bootstrap(): Promise { + const app = await NestFactory.create(AppModule); + app.setGlobalPrefix('api'); + app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); + app.enableCors({ origin: 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/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/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7f701d0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5475 @@ +{ + "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": ">=22" + } + }, + "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" + } + }, + "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/@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/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/@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/@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/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/@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/@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/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/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/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/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/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/@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-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/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/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/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/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/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/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/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/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-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/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/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/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/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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-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-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "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/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-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-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/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/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-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-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-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-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-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-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/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/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==", + "dev": true, + "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/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/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/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.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/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/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/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/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/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-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/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/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/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/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-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/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-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/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/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/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/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-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/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/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/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/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/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/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/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-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/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/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/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/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/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/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/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-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/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/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" + } + } + } +} From 706c57355094c610781385f89a09a017033ef4e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 16:04:00 +0000 Subject: [PATCH 06/68] =?UTF-8?q?feat(pillar-auth):=20standalone=20auth=20?= =?UTF-8?q?=E2=80=94=20JWT=20login,=20claims=20RBAC,=20field-role=20offlin?= =?UTF-8?q?e=20grace,=20Google=20stub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/src/pillars/auth/auth.controller.ts | 45 +++++++++++++ apps/api/src/pillars/auth/auth.module.ts | 25 +++++++ apps/api/src/pillars/auth/auth.service.ts | 69 ++++++++++++++++++++ apps/api/src/pillars/auth/user.entity.ts | 28 ++++++++ 4 files changed, 167 insertions(+) create mode 100644 apps/api/src/pillars/auth/auth.controller.ts create mode 100644 apps/api/src/pillars/auth/auth.module.ts create mode 100644 apps/api/src/pillars/auth/auth.service.ts create mode 100644 apps/api/src/pillars/auth/user.entity.ts 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..d90ed78 --- /dev/null +++ b/apps/api/src/pillars/auth/auth.controller.ts @@ -0,0 +1,45 @@ +import { Body, Controller, Get, HttpCode, NotImplementedException, Post } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import type { UserClaims } from '@ubi/types'; +import { IsEmail, IsString, MinLength } from 'class-validator'; +import { CurrentUser, Public } from '../../common/rbac'; +import { AuthService, LoginResult } from './auth.service'; + +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(@Body() dto: LoginDto): Promise { + 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..8dc7b65 --- /dev/null +++ b/apps/api/src/pillars/auth/auth.module.ts @@ -0,0 +1,25 @@ +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) => ({ + secret: config.get('JWT_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..3d24a09 --- /dev/null +++ b/apps/api/src/pillars/auth/auth.service.ts @@ -0,0 +1,69 @@ +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; + claims: string[]; + isField?: boolean; + entity?: 'UBI' | 'OMEGA'; + }): Promise { + const existing = await this.findByEmail(input.email); + if (existing) return existing; + const user = this.users.create({ + email: input.email.toLowerCase(), + name: input.name, + 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; +} From 6e6c042c2d6fac5ee7602801e80a18bac53fbf6e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 16:04:00 +0000 Subject: [PATCH 07/68] =?UTF-8?q?feat(pillar-doc-tracking):=20document=20r?= =?UTF-8?q?egistry=20=E2=80=94=20QR=20codes,=20scan=20transmit/receive,=20?= =?UTF-8?q?where-is=20+=20full=20movement=20history?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- .../doc-tracking/doc-tracking.controller.ts | 59 ++++++++ .../doc-tracking/doc-tracking.module.ts | 15 ++ .../doc-tracking/doc-tracking.service.ts | 133 ++++++++++++++++++ .../doc-tracking/document-movement.entity.ts | 29 ++++ .../pillars/doc-tracking/document.entity.ts | 30 ++++ 5 files changed, 266 insertions(+) create mode 100644 apps/api/src/pillars/doc-tracking/doc-tracking.controller.ts create mode 100644 apps/api/src/pillars/doc-tracking/doc-tracking.module.ts create mode 100644 apps/api/src/pillars/doc-tracking/doc-tracking.service.ts create mode 100644 apps/api/src/pillars/doc-tracking/document-movement.entity.ts create mode 100644 apps/api/src/pillars/doc-tracking/document.entity.ts 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[]; +} From 1197845b1a9226210a3b365e18488a5c49632281 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 16:04:00 +0000 Subject: [PATCH 08/68] feat(pillar-ai-layer): read-model registry, grounded ask endpoint, off-box insight postback gate https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- .../pillars/ai-layer/ai-layer.controller.ts | 100 ++++++++++++++++++ .../src/pillars/ai-layer/ai-layer.module.ts | 15 +++ .../src/pillars/ai-layer/insight.entity.ts | 31 ++++++ .../pillars/ai-layer/read-model.registry.ts | 26 +++++ 4 files changed, 172 insertions(+) create mode 100644 apps/api/src/pillars/ai-layer/ai-layer.controller.ts create mode 100644 apps/api/src/pillars/ai-layer/ai-layer.module.ts create mode 100644 apps/api/src/pillars/ai-layer/insight.entity.ts create mode 100644 apps/api/src/pillars/ai-layer/read-model.registry.ts 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); + } +} From d7f536e2567bec0d11f84929d1de4b81f40ebe7f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 16:04:00 +0000 Subject: [PATCH 09/68] feat(api/shared): RBAC-gated files via StorageProvider + approvals/ticketing/notifications/expiry engines https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- .../src/shared/approvals/approval.entity.ts | 41 ++++++++ .../shared/approvals/approvals.controller.ts | 30 ++++++ .../src/shared/approvals/approvals.module.ts | 14 +++ .../src/shared/approvals/approvals.service.ts | 83 ++++++++++++++++ apps/api/src/shared/expiry/expiry.module.ts | 86 ++++++++++++++++ apps/api/src/shared/files/file.entity.ts | 30 ++++++ apps/api/src/shared/files/files.controller.ts | 63 ++++++++++++ apps/api/src/shared/files/files.module.ts | 30 ++++++ apps/api/src/shared/files/files.service.ts | 47 +++++++++ apps/api/src/shared/files/storage.token.ts | 2 + .../notifications/notifications.module.ts | 72 ++++++++++++++ .../src/shared/ticketing/ticketing.module.ts | 99 +++++++++++++++++++ 12 files changed, 597 insertions(+) create mode 100644 apps/api/src/shared/approvals/approval.entity.ts create mode 100644 apps/api/src/shared/approvals/approvals.controller.ts create mode 100644 apps/api/src/shared/approvals/approvals.module.ts create mode 100644 apps/api/src/shared/approvals/approvals.service.ts create mode 100644 apps/api/src/shared/expiry/expiry.module.ts create mode 100644 apps/api/src/shared/files/file.entity.ts create mode 100644 apps/api/src/shared/files/files.controller.ts create mode 100644 apps/api/src/shared/files/files.module.ts create mode 100644 apps/api/src/shared/files/files.service.ts create mode 100644 apps/api/src/shared/files/storage.token.ts create mode 100644 apps/api/src/shared/notifications/notifications.module.ts create mode 100644 apps/api/src/shared/ticketing/ticketing.module.ts 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..044c239 --- /dev/null +++ b/apps/api/src/shared/ticketing/ticketing.module.ts @@ -0,0 +1,99 @@ +import { Body, Controller, Get, Global, Injectable, Module, Param, Post, Query } 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, Repository } from 'typeorm'; +import { BaseAppEntity } from '../../common/base.entity'; +import { CurrentUser } 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); + } +} + +@Global() +@Module({ + imports: [TypeOrmModule.forFeature([TicketEntity])], + controllers: [TicketingController], + providers: [TicketingService], + exports: [TicketingService], +}) +export class TicketingModule {} From 427361ad55e2054723b5a24f014304b977d39603 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 16:04:00 +0000 Subject: [PATCH 10/68] feat(api/modules): engineering core + procurement DR + operations tower + dept read-model stubs + CEO ManCom consolidation + seeds All 14 departments expose Pillar-3 read surfaces; ManCom snapshot verified end-to-end against live Postgres (RBAC 401/403 checked). https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/src/app.module.ts | 62 ++++++ apps/api/src/modules/ceo/ceo.module.ts | 113 ++++++++++ .../engineering/engineering.controller.ts | 50 +++++ .../modules/engineering/engineering.module.ts | 13 ++ .../engineering/engineering.service.ts | 113 ++++++++++ apps/api/src/modules/engineering/entities.ts | 55 +++++ .../modules/operations/operations.module.ts | 82 +++++++ .../modules/procurement/procurement.module.ts | 142 ++++++++++++ apps/api/src/modules/stubs/stubs.module.ts | 205 ++++++++++++++++++ apps/api/src/seed/seed.module.ts | 127 +++++++++++ 10 files changed, 962 insertions(+) create mode 100644 apps/api/src/app.module.ts create mode 100644 apps/api/src/modules/ceo/ceo.module.ts create mode 100644 apps/api/src/modules/engineering/engineering.controller.ts create mode 100644 apps/api/src/modules/engineering/engineering.module.ts create mode 100644 apps/api/src/modules/engineering/engineering.service.ts create mode 100644 apps/api/src/modules/engineering/entities.ts create mode 100644 apps/api/src/modules/operations/operations.module.ts create mode 100644 apps/api/src/modules/procurement/procurement.module.ts create mode 100644 apps/api/src/modules/stubs/stubs.module.ts create mode 100644 apps/api/src/seed/seed.module.ts diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts new file mode 100644 index 0000000..d0917ec --- /dev/null +++ b/apps/api/src/app.module.ts @@ -0,0 +1,62 @@ +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 { EngineeringModule } from './modules/engineering/engineering.module'; +import { OperationsModule } from './modules/operations/operations.module'; +import { ProcurementModule } from './modules/procurement/procurement.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, + ProcurementModule, + OperationsModule, + StubsModule, + CeoModule, + SeedModule, + ], + controllers: [HealthController], + providers: [{ provide: APP_GUARD, useClass: RbacGuard }], +}) +export class AppModule {} 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..9703786 --- /dev/null +++ b/apps/api/src/modules/ceo/ceo.module.ts @@ -0,0 +1,113 @@ +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 { 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, + @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; + + 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, + }, + accomplishment, + surveyVolumes, + flagged, + reportFeed: seedReportFeed(), + topInsights, + }; + } + + 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], + controllers: [CeoController], + providers: [CeoService], +}) +export class CeoModule {} 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..24e7776 --- /dev/null +++ b/apps/api/src/modules/engineering/entities.ts @@ -0,0 +1,55 @@ +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; +} + +/** 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/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..192ab87 --- /dev/null +++ b/apps/api/src/modules/procurement/procurement.module.ts @@ -0,0 +1,142 @@ +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'; + +/** + * 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, + 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(), + ); + } + + 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(); + } +} + +@Module({ + imports: [TypeOrmModule.forFeature([DeliveryReceiptEntity])], + controllers: [ProcurementController], + providers: [ProcurementService], + exports: [ProcurementService], +}) +export class ProcurementModule {} 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..bbfc325 --- /dev/null +++ b/apps/api/src/modules/stubs/stubs.module.ts @@ -0,0 +1,205 @@ +import { Controller, Get, Injectable, Module, OnModuleInit } from '@nestjs/common'; +import type { ReportStatus, TrendPoint } 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 { + this.registry.register( + { + key: 'survey.volumes', + module: 'survey', + title: 'Monthly survey volumes (m³ ×1000)', + description: 'Feeds ManCom consolidation; station/chainage indexed.', + }, + (): TrendPoint[] => [ + { period: 'Jan', plan: null, actual: 44, projected: null }, + { period: 'Feb', plan: null, actual: 54, projected: null }, + { period: 'Mar', plan: null, actual: 59, projected: null }, + { period: 'Apr', plan: null, actual: 50, projected: null }, + { period: 'May', plan: null, actual: 64, projected: null }, + { period: 'Jun', plan: null, actual: 31, projected: null }, + { period: 'Jul', plan: null, actual: null, projected: 61 }, + ], + ); + + this.registry.register( + { + key: 'mqc.certs', + module: 'mqc', + title: 'Material certificates queue', + description: 'DPWH minimum testing per pay item; certs attach to billings.', + }, + () => [ + { certId: 'MQC-2026-0712', payItemNo: '311(1)c', test: 'Concrete beam (7-day)', status: 'pending', daysPending: 6, billing: 'Billing No. 7 / PKG-02' }, + { certId: 'MQC-2026-0713', payItemNo: '311(1)c', test: 'Concrete beam (14-day)', status: 'pending', daysPending: 6, billing: 'Billing No. 7 / PKG-02' }, + ], + ); + + 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' }, + ], + ); + + this.registry.register( + { + key: 'it.capacity', + module: 'it', + title: 'Capacity planning & controls', + description: 'Resource headroom + projected breach dates (scaling triggers).', + }, + () => [ + { 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' }, + ], + ); + + this.registry.register( + { + key: 'records.expiry', + module: 'records', + title: 'Expiring registrations & insurance', + description: 'Vehicle reg, insurance, stamps — alarm before lapse.', + }, + () => this.expiry.upcoming(30), + ); + + 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: 'property.assets', + module: 'property', + title: 'Asset custody (QR EAM)', + description: 'Who holds what; monthly custodian attestation trail.', + }, + () => [ + { qr: 'AST-00112', type: 'Total station', custodian: 'Survey — Reyes', location: 'PKG-02 site office', status: 'in-use' }, + { qr: 'AST-00387', type: 'Plate compactor', custodian: 'PKG-05 warehouse', location: 'PKG-05', status: 'in-use' }, + ], + ); + + this.registry.register( + { + key: 'finance.doc-flow', + module: 'finance', + title: 'Document batches in motion', + description: 'Where is this paper now — batches to cashier, incoming from Records.', + }, + async () => { + const docs = await this.docs.list(); + return { + inTransit: docs.filter((d) => d.status === 'in-transit').length, + withFinance: docs.filter((d) => d.currentHolder.toLowerCase().includes('finance')).length, + recent: docs.slice(0, 10), + }; + }, + ); + } +} + +/** 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` }; + } + + @Get('survey/volumes') survey(): unknown { return this.query('survey.volumes'); } + @Get('mqc/certs') mqc(): unknown { return this.query('mqc.certs'); } + @Get('audit/exceptions') audit(): unknown { return this.query('audit.exceptions'); } + @Get('it/capacity') it(): unknown { return this.query('it.capacity'); } + @Get('records/expiry') records(): unknown { return this.query('records.expiry'); } + @Get('clinic/aggregate') clinic(): unknown { return this.query('clinic.aggregate'); } + @Get('hr/hours') hr(): unknown { return this.query('hr.hours'); } + @Get('property/assets') property(): unknown { return this.query('property.assets'); } + @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/seed/seed.module.ts b/apps/api/src/seed/seed.module.ts new file mode 100644 index 0000000..25c81d8 --- /dev/null +++ b/apps/api/src/seed/seed.module.ts @@ -0,0 +1,127 @@ +import { Injectable, Logger, Module, OnApplicationBootstrap } from '@nestjs/common'; +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 { EngineeringModule } from '../modules/engineering/engineering.module'; +import { PayItemEntity, ProjectEntity } from '../modules/engineering/entities'; +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 auth: AuthService, + private readonly docs: DocTrackingService, + private readonly expiry: ExpiryService, + @InjectRepository(PayItemEntity) private readonly payItems: Repository, + @InjectRepository(ProjectEntity) private readonly projects: Repository, + @InjectRepository(InsightEntity) private readonly insights: Repository, + ) {} + + async onApplicationBootstrap(): Promise { + if ((await this.payItems.count()) > 0) return; + this.log.log('empty database — seeding demo data'); + + await this.auth.ensureUser({ email: 'admin@ubi.ph', name: 'Suite Admin', password: 'admin123', claims: ['role:admin'] }); + const ceo = await this.auth.ensureUser({ email: 'ceo@ubi.ph', name: 'E. Castillo', password: 'ceo123', claims: ['role:ceo'] }); + await this.auth.ensureUser({ email: 'vpo@ubi.ph', name: 'VPO Office', password: 'vpo123', claims: ['role:vpo', 'dept:operations'] }); + await this.auth.ensureUser({ email: 'pm@ubi.ph', name: 'PM Uy', password: 'pm123', claims: ['role:pm', 'dept:engineering'] }); + await this.auth.ensureUser({ + email: 'pe@ubi.ph', name: 'Engr. R. dela Cruz', password: 'pe123', + 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)', password: 'agent123', claims: ['agent:insights'] }); + + 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 ceoClaims: UserClaims = this.auth.toClaims(ceo); + 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'); + + await this.expiry.add({ kind: 'vehicle-reg', refModule: 'records', title: 'Dump truck UBI-DT-114 registration', expiresOn: '2026-06-24' }); + await this.expiry.add({ kind: 'insurance', refModule: 'records', title: 'Service vehicle UBI-SV-09 insurance', expiresOn: '2026-06-22' }); + + this.log.log('seed complete — login ceo@ubi.ph / ceo123 (also admin, vpo, pm, pe, insights-agent)'); + } +} + +@Module({ + imports: [DocTrackingModule, EngineeringModule], + providers: [SeedService], +}) +export class SeedModule {} From 295df16c6695ba5da293ce5ff6e0fe7324768791 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 16:04:01 +0000 Subject: [PATCH 11/68] =?UTF-8?q?feat(web):=20React+Vite=20PWA=20shell=20?= =?UTF-8?q?=E2=80=94=20brand=20light=20UI,=20role-aware=20sidebar,=20auth?= =?UTF-8?q?=20session,=20chart=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/index.html | 13 + apps/web/package.json | 26 + apps/web/public/icon-192.png | Bin 0 -> 526 bytes apps/web/public/icon-512.png | Bin 0 -> 6987 bytes apps/web/src/App.tsx | 36 + apps/web/src/Shell.tsx | 88 + apps/web/src/api.ts | 50 + apps/web/src/charts.tsx | 123 + apps/web/src/main.tsx | 13 + apps/web/src/styles.css | 183 ++ apps/web/tsconfig.json | 13 + apps/web/vite.config.ts | 30 + package-lock.json | 5722 ++++++++++++++++++++++++++++++++-- 13 files changed, 6027 insertions(+), 270 deletions(-) create mode 100644 apps/web/index.html create mode 100644 apps/web/package.json create mode 100644 apps/web/public/icon-192.png create mode 100644 apps/web/public/icon-512.png create mode 100644 apps/web/src/App.tsx create mode 100644 apps/web/src/Shell.tsx create mode 100644 apps/web/src/api.ts create mode 100644 apps/web/src/charts.tsx create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/styles.css create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/vite.config.ts diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..12495f3 --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,13 @@ + + + + + + + UBI Suite + + +
+ + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..d956bfa --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,26 @@ +{ + "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": { + "@ubi/types": "*", + "react": "^18.3.1", + "react-dom": "^18.3.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 0000000000000000000000000000000000000000..986a4a2df590d0ad53b880b7f2153aee25073989 GIT binary patch literal 526 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1SD_us|YYKFdp`FaSW-L^X8_t|Dgbp10RE{ zG=t=Hg}U7qc_v>t>e_qkprH9LS`0^@f)e8&#zYe^{wURbclYc*$^gzyA~?^~8z z{$J_c9-sGKTeNL2Z=#QbN3z6L2{+xg!0yB=NqCvsFE*^XwR!Q|H3dFbPp()eSs0X6 zKS{iqBiLrWK=#{L@2x*BTU(mAs)^NG!|&t@apCoCGcqEMiHOW#dl$H?Zc?4+O1(2m z{}Tm2be%l5^5dMp-!=Z8NuROq%lb9H&+ZgEV`ZQCvZHwR?&!H?l@jywH@IHhyK$BG zMY$6vDo}E*9}5iZ)*N=ZaCB|`yyX@qgg^L%Wo_5xv5zV5<+hbZf%h;vP2MZQt&~gZo|e6jm5%gTe~DWM4f14HV6 literal 0 HcmV?d00001 diff --git a/apps/web/public/icon-512.png b/apps/web/public/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..c27099d40ab34097ec32e8259b6c1ef1a81943d6 GIT binary patch literal 6987 zcmeHs=T}op+xH}JC<210R4E>sO7EQ@ASeiUq=OU%By^-I0TPrVilFq4h=73fCM^UI zk(vl1(xXW4H9|u2#B=`v&%56BuJwMozwCYOYi7;tnKk?Rl^u7>#DL`#&nW-^%Z=-L zw*i1sLnvVQb3^dxRsg^TZs=V#f0(mAf*LoyR+GTp1fVzpc5})O6yVF)jkkjRYs=!+S4{_mf=f1fS@=l#CLN+GWW2G6=tC zx_oCQm5`u+Nw%;u!D4J{FGnrFcJHb|i3Nq&NdWWV48`t07YjDYeJNQnXkJ@tD|KFNv@gG5 z_!ib}6|!hrkaZ>6ye3a-Y@{dj(%5Kh8Q;`E?v$bZlwouG*swM)JF2lp$t={+>PP*w zdF>3WpzeEaW1QQLaO=6FmthMYiKKxN3MsbF)&p_5sUnByUbEI%O48DVpDm3{#xm8H zrDpEhGS0q}mX$n?$nSwQ+OH7jZLfo}UvvMmsLg*>0Yyr-Q)J zXKQ`BwuQdvbI<|_yxxY2#JTN^1$gJ)?RLlr=(Y4fmb8` z2CVPK)|Ag3qh(rtx)p6={$@w@jHXwT4tG1<4io;u!z=l=h%jBxPj@8ht>x?o#_2g` zU5{QhRm_I#kDG&pA8&_8>Ja*@U)37ZbI;f^L#y%MTnv3}+leq1=Ah;f&f?_wK9Qpx zdH>`P(;(BY@{!TPfa#J&RmVHza-!wd?5_x^jJTkQ-GESo;?>gpbFGh=L^$GsL`tRk z!GSnIRGn-y=aA9t;y1h|^?vQrj9O|HEvKP01H_uGB-jLv*ch&B5u=fz?<>O;9o0+~ z?+RJQaH7CqhvSC&er)+6bKqB~j9Yl-WE~>Lpe$3;l{0Qw9+F|oS8#ZoibxvOaxV#Y zA_ujY^D8HUUa7Y^-OZV{Q7iY{jp>{9pGm@y*ic6g z?i8ow?879jm@$uSM*nH9Mvx4WHRmhYa#^7=tiyxnXB+M>*<$@-qLp8kl5adXJDv?`Jq+&fvrutKml}`|oXT<`>ZMo<~v?Ajifee0-a+v}Ai*|6Tp+}@xRsJt7mlC${8LP%wE2W(#75zg2r zgAN!Pc9Efx7Y6dlj0Z4>I!{*vMIh;H-$?qV*sA_`rZ)2oD)Z4nZKv3WDJEO&N^_}| zQZ($lqew_dyy+D(2UvA++=B9 z6*iR8E9Ui2r#U@unu212y7-~1#ujAKK(4yVzD8Px50dd|L9;t=m(_bIXS2`cAx)wY z;5Q8^J6?4)A#2+i#wc_Dz|=5e=9~TT7*uvdfu=||qG+tOR|NE?ygxy|MqUEm;@o%3 zI}>$5!iwt&eZ`56oss_U)I8P)UQoZlHMM7)3%xYB#iXmVctJcDEq>YI8}gDXL15BR z?z8j;?3@Y@MCF+afs!7o53Lp!o^O+z_l6i;1O_`l(HFNuqeO(bmpBQk3@WD}58p4W z^w))Mkh_z1r_n!VQ%a(=(tyUd=f|fXO-M_~_TB2Jo_o=m=xRoQ9~_HlIzz1er3Mn| z65}EiAP-aOT13ZP`NX=|>(O;N%n$KrXz~9@4I!3c5;k-sSz7$v%v&Abha9T|H7ndC zxc4wvMohQVGCyHi*}5yu&7&`aKEed>2RCPTIF%w|>a-ID0c(G;$I_x0wEAuCsQu>{ zTRa51{aRkFL~Vw|WMuDH9IWqiWg9>q;*wiWL7t-SBA)_{Wr;b)r%(Qb7t$m^PF_GP zgWeCu+~hP!0voDZ0%&XauQ+kz86YZys#l*rdj)$V#Maek(@Mj3MhF=NE;QEEokPL7 zj)Zcch7o@q1o5F+*j~B(8s$GF4C2Nz{XY@sE{gVjt~yL}f?W6dJb4nk%taCeaWByH zYywXNB%oqSwgC0}{ex520eidq7C-#QYtb%HEFa~Xv zHX-~uwtuq>)Deoj`d>If{eC0ZP~QWKbD->Cj^=67HM}7B;Rm2>yiWwEsBQbdp*Ch; ziF~|f8aji}*}J(h5j>je7OnK9f&7+9D+mEE6QB&cI%oSbZh!bM1^E9=ePr50+xfGu zq#8yCkB}&np%pw%YEaDn>cTyEnYo)4Tp)8Z59ie|Vvbs20HfupSr?$q+YTe0j7!45{-8hfpP=UTPNubX#QNsjgMbN%n{ajqzj1!eRn#FmImFoOwg!5v$ zbwT@wu}ZI2d?J7cRGWqtwbIb_?4AKu8-6ykG7Nr29Ha1=2aI|~vlxo!Sh?RCO$1pH zV;mbg)TtW&GJ&|ZWFy^;pJbt3?8Q@|3fk1P>n|sGV{yKf`yFcMEA!nD9w!#m%6Y<8 znU#1!shU+4tM{9Lo=Kef!jBUOk04NZ8whuVmu?Sl7?bm_wXy-8+x$Gt z#$)X}DLB!V@qDhVvXzEnduy847l&X_>+xSRt8_v8Rm;nc+*xITa96`)g>dU~A^2TD-z>opToDdvrJk~}Zx0z=`AE?>0%tTv{;%YA`#!!0Il{8VZd z&ouXI2vl2Fbc^B+ZzL6qMWv1J_pI8wdw7he0T$}}$b}MEbt^cs%C_Q`VmZNZ9cBVz?D~1mw^${xC zejgP?Q?HK-CPW)&&%c(1Y8Q%bwyG;kJ$p`ZhTk2KZu?td>Lb>N6~{$@;NHbmgv8vp zdvJBHsk%1_wi9Q+X2LwA)OeHzUOb@HF8s9FuBc!ae6$)FAjKl-KC~dsGGsa3I#a0a zo&#|2O6~2+&I%PwSZba`7l=r9Y^zeIeGlZ%LNUzovjG9{Mn!Cd&1hBq(KQ-3#}UM& znNHn{1`I`T@+pv^=~i~+&8jFw_Api*&*Scy9OVlzNaFDaR`l=;YBRBexPXIxYu{G1 z=e1p4!S%M|6*Wt8&Sy#506&&X@_&Ot?FVxw$?93iLia0Ek9OB)`D}Gt%9lQllWxEv z-AxhFn41hhUVguDD~6+bH;I#3qew)GescervTJ(9DM>uen8M>EN{hxoW_B2Hao7u~wCGe9~cB)*{!}!G22{dtqt= zq!A;nu>8&bSPq%cC06L*OCY0ra9kh|CSM;T#PYd9Q%j;n$c}O|#23y&kES#LD$yAH zzF8F}Q6FEd@Ve;mN7TtOT>tarV*6%U9r3~i(~!I^O0mR zU#*pn_PRo(D_piPWM=scZZM(usyuIMO##guaWzXkLa07(fz#o^>=8EWd*h?^P2O1F z)`;JWYyCp>U4uaal&FOzSRLj4b0W4)rs=*j+=7TcS^;%QJSxq7RWjZ!<)+)G$0n9l z7y6dSGb{ys!r~k^n2Qf*949+kKOo1y;)fl>yhE{<-4-k>OqKR?#m7Yr*iTGbE;WVf z!XNdtPa%KriqYR+h@LWKeGov4kudgZ4wzHSw;|@F$EJ-3>?S8+oyoNt3f5{LlylcM z@6?En%Ck+5CYUyS)Ln*~eFGWod{*sMe_26!QtZ*iOJD0+6JZtMP2^~A!urXv1?0XF zwM(hy)FF95jK;Y3piq)v?KSxv$cRlOO-4>HR6@{gSS<>@o#H|KMOhRnB#I9 zrbRuHsaY7_$Pe>%`Y|THFtl|vG--Yjlfwx1=E>NIOEziD64gZ5-c>PVhBOq@HbI)5 zD~snpC6Q4x)a2FC|B7ReI;Ctz0iV~qfFi^r(3je-N*C!9*_Mb+d~!;feiN0kuX-LPX5 z)L(*BMN@&?OOq5J>#jlSEV+4GPF`T=kQDV~H zayK6!AXF_>U47aCd9cwzX&Z^ujEoU+Bl(^PU?sffE;wCe1WZ08F&KGgORL;(ZYGlM z!f&#bFQPkOK$l1(Ap>!eJx3oR+FIX=-X1^j5b*(>T04&%@YV9hjtQ_XWluhZ+O1!c4*; z9UBXM3CI=vEO>q$SO*>)Cm>06*^c3zXy33h2dU_%sRpOlKdgo;pNyA?8ZAIL>^<R=$;F942h(;X_<2ioK{E$9~-8wd0z3FYdgxVnra7EEpl@<-ZcNr9 z#?xZSvW(j7Pe^g5gTA3Ugcvh_&qQp6ZF>J=SJyj}_?sJ3HW%8E{!3|H35eu?N*lGFQ{9`W001htJar%Ko!wg!-4`|JY7IIYR$%bx#oIuX!HznTv#H5WJwpK1eGl zM+Z}ZUcYH>!S=>^`w58ZE`jnf>I6zasdS&&;)GpLEtVM#go7>BXxK8Twv3)B=o+pC z+JVdwY^aY?RGOdI$tp!-yk2#`JSZeuS1;+++}635K-lAAL$4h8D9nB2vaP#Mi?7ZV zfuNpJ1qwB+Sf~Vg-Jb<6g4h|=R90<;c+wfD-}l9PRy7Zp2mhF?g;{;ELo&5yu5_u zzXtf%vYlys7*Haj+^bbDq?k0as4BF;_K?>ZeX07bu&(AmA;E+iTKpxnLd%=mdr8-= z|4w>9{<++bk!C{`UOx>*dy0UxXD&Ih9>J7E9gwh@uW$La{WAZgV=a+#^yPeH;Tcrn zl>N|*wO_AbeLL@|aKx2n%ohfXti57e%c>xlw2|w|=eIepLI+VP&P|<$B}%bsPZ{bz zOrN1RpGEa1udBzK%R%6}!~yPBN}?&?$GT|l#ocujgd62^IV8vX;lI;5@yz8}AI{&v zvPLQeOt~}}bQl{^U4j~2&k8zS-Wzm>zNaf#ZW@5%^!Ieg*-SmSKp)1I=p6+Veg$-k z=Q2EQe3?VKL8pWRRF6V4^%x&{5PK!U9ybzGwgQ7yqc>}?FKVuNe)X%E*QFmHm!Q)w zH~h4RCHQh3e$S6##4t}QjqMekVLczoanStAqKR`alA0Y13w0SYea4agdO_s?#srm- zPkkAI#m8tacmZ2|xg-I#>fusKCOe8--FyJ;?Kck}t2%9~BNyLyUGxce#zniDiSKqR zVZG=V4t&hUR_}_F?%vdwlL^SITv(%#Pha=(5?_*9;Wt)ajS?;Wy1J!Ny_1&|J!iv? znwYZa+bDOD-sEP*yeS`BLq77Gd666*Il+!nov_?gGw;ipQruJl9A*s;l(i?l_IB1B z8B~j@TykNvqG&q$Xy-jG=1oRf(QGTS`yiSBP}PcRYXw{R!}10la|8y8X2h78Jl`fA zC3n6EIgfw_h=M&Ko3rRgwS>o&HF@!L7@Y~n;kHM9qY3_2PFf6@Hty0Ol3l^pW&5)O z@?&7IX`^V}j$V1As^w?EECVL&%2)Yu<9$L&dCB54NCs@~!Wxni$>PkvuT(|rlxLxH zT--M+*)wg{BGw|oh{<8GqJnJ4=76)fxX+zjUud;pu!aA40;|{OG^P6kkchJZaaVL>l(mH0yjzU>dwtkahgMK#_WYvg2<%-DSXZP1a5yQaC@P#yFT1c z=!Hv&9a@d+&kMyrtyS*cF^A$V$brt{jtSb=nJy^!#C{VL0@vaeyrOhcp-Vk5?Gg9K zf4LrgF+zL_m7i7WyDjfv?C7{m9DbPpT-`1f)aNAJQg8D7Sv&iMskJwtlJm{bdCwok zedw*5`!jMA^ERg|^ntCF(pM9#v8Jq*io9|r3Aa>@Qj)bbo-S`Cd6{{+3;-7XC5phRJ;VxSaD?V6B(BUn?l?Q&Ok82Eq*D#PMIN; +} + +export default function App() { + const user = getUser(); + const home = hasClaim(user, 'role:ceo') || hasClaim(user, 'role:vpo') ? '/mancom' : '/approvals'; + return ( + + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + + ); +} diff --git a/apps/web/src/Shell.tsx b/apps/web/src/Shell.tsx new file mode 100644 index 0000000..06c2bf1 --- /dev/null +++ b/apps/web/src/Shell.tsx @@ -0,0 +1,88 @@ +import { NavLink, Outlet, useNavigate } from 'react-router-dom'; +import { clearSession, getUser, hasClaim } from './api'; + +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..087fafc --- /dev/null +++ b/apps/web/src/api.ts @@ -0,0 +1,50 @@ +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')); +} diff --git a/apps/web/src/charts.tsx b/apps/web/src/charts.tsx new file mode 100644 index 0000000..57cb6d4 --- /dev/null +++ b/apps/web/src/charts.tsx @@ -0,0 +1,123 @@ +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 && ( + <> + + + 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..3520189 --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App'; +import './styles.css'; + +createRoot(document.getElementById('root')!).render( + + + + + , +); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css new file mode 100644 index 0000000..05f2492 --- /dev/null +++ b/apps/web/src/styles.css @@ -0,0 +1,183 @@ +/* UBI Suite — brand light UI (per approved mockups.html) + navy chrome · orange action · teal data · blue AI */ +:root { + --ink: #0f2233; + --orange: #c2410c; + --teal: #0e7490; + --blue: #1d4ed8; + --ok: #15803d; + --warn: #b45309; + --bad: #b91c1c; + --muted: #5b6b7a; + --bg: #f3f5f7; + --line: #e3e8ee; +} +* { box-sizing: border-box; } +html, body, #root { margin: 0; height: 100%; } +body { + background: var(--bg); + color: var(--ink); + font-family: 'Inter', 'Segoe UI', system-ui, sans-serif; + 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: ui-monospace, Menlo, monospace; font-size: 0.92em; } + +/* ── shell ─────────────────────────────────────────── */ +.shell { display: flex; min-height: 100vh; } +.side { + flex: 0 0 216px; background: var(--ink); color: #cfdae4; + display: flex; flex-direction: column; padding: 14px 0 12px; + position: sticky; top: 0; height: 100vh; overflow-y: auto; +} +.sbrand { display: flex; align-items: center; gap: 9px; padding: 2px 16px 14px; border-bottom: 1px solid #1d3a54; } +.slogo { + width: 30px; height: 30px; border-radius: 7px; background: var(--orange); color: #fff; + display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 15px; +} +.sbrand b { display: block; color: #fff; font-size: 14px; line-height: 1.15; } +.sbrand span { font-size: 9.5px; color: #8fa6b8; letter-spacing: 0.8px; text-transform: uppercase; } +.sentity { + margin: 12px 14px 2px; background: #1c3954; border: 1px solid #2c4d6c; border-radius: 6px; + padding: 7px 10px; font-size: 11.5px; color: #dbe7f1; display: flex; justify-content: space-between; +} +.sentity b { color: #fff; } +.sgrp { padding: 13px 16px 5px; font-family: ui-monospace, monospace; font-size: 9px; letter-spacing: 1.6px; color: #7e96aa; text-transform: uppercase; } +.sitem { + display: block; padding: 7px 16px; font-size: 13px; color: #cfdae4; + border-left: 3px solid transparent; +} +.sitem:hover { background: #16314a; color: #fff; } +.sitem.on { background: #1c3954; color: #fff; border-left-color: var(--orange); font-weight: 600; } +.sitem.dim { color: #8fa6b8; font-size: 12.5px; padding: 5.5px 16px; } +.suser { margin-top: auto; border-top: 1px solid #1d3a54; padding: 11px 16px; display: flex; gap: 9px; align-items: center; } +.suser .av { width: 28px; height: 28px; border-radius: 50%; background: #2c4d6c; color: #fff; font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex: none; } +.suser b { display: block; color: #fff; font-size: 12px; line-height: 1.2; } +.suser span { font-size: 9.5px; color: #8fa6b8; } +.suser button { margin-left: auto; background: none; border: 1px solid #2c4d6c; color: #8fa6b8; border-radius: 5px; font-size: 10.5px; padding: 3px 8px; } +.suser button:hover { color: #fff; border-color: #8fa6b8; } + +.main { flex: 1; min-width: 0; padding: 20px 24px 32px; } +.top { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 16px; } +.top h1 { margin: 0; font-size: 21px; font-weight: 800; } +.top .right { margin-left: auto; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } +.asof { font-size: 11.5px; color: var(--muted); display: flex; align-items: center; gap: 6px; } +.asof i { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); } + +/* ── components ────────────────────────────────────── */ +.btn { + border: 1px solid var(--line); background: #fff; color: var(--ink); + border-radius: 7px; padding: 7px 14px; font-weight: 600; font-size: 13px; +} +.btn:hover { border-color: var(--muted); } +.btn.pri { background: var(--orange); border-color: var(--orange); color: #fff; } +.btn.pri:hover { background: #a83709; } +.btn.navy { background: var(--ink); border-color: var(--ink); color: #fff; } +.btn.ok { background: var(--ok); border-color: var(--ok); color: #fff; } +.btn.sm { padding: 4px 10px; font-size: 11.5px; } +.btn:disabled { opacity: 0.5; cursor: default; } + +.card { background: #fff; border: 1px solid var(--line); border-radius: 9px; padding: 14px 16px; } +.ph { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; } +.ph b { font-size: 13.5px; } +.src { + margin-left: auto; font-family: ui-monospace, monospace; font-size: 9px; letter-spacing: 0.6px; + color: var(--blue); background: #eef3ff; border: 1px solid #ccdaf8; border-radius: 4px; + padding: 1px 7px; white-space: nowrap; +} + +.st { font-size: 10px; font-weight: 700; border-radius: 10px; padding: 2px 9px; white-space: nowrap; } +.st-ok { background: #e9f6ee; color: var(--ok); border: 1px solid #c4e5d1; } +.st-warn { background: #fdf3e7; color: var(--warn); border: 1px solid #f0d7b5; } +.st-bad { background: #fdecec; color: var(--bad); border: 1px solid #f3c2c2; } +.st-info { background: #eef3ff; color: var(--blue); border: 1px solid #ccdaf8; } + +.tbl { width: 100%; border-collapse: collapse; font-size: 12.5px; } +.tbl th { + font-family: ui-monospace, monospace; font-size: 9.5px; letter-spacing: 1px; text-transform: uppercase; + color: var(--muted); text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--line); font-weight: 600; +} +.tbl td { padding: 7px 8px; border-bottom: 1px solid #eef2f5; vertical-align: middle; } +.tbl tr:last-child td { border-bottom: 0; } +.neg { color: var(--bad); font-weight: 700; } +.pos { color: var(--ok); font-weight: 700; } + +.field { display: block; margin-bottom: 12px; } +.field span { display: block; font-size: 11px; font-weight: 600; color: var(--muted); margin-bottom: 4px; letter-spacing: 0.3px; } +.field input, .field select { + width: 100%; border: 1.4px solid var(--line); border-radius: 7px; padding: 9px 12px; font-size: 14px; +} +.field input:focus { outline: none; border-color: var(--ink); } + +.toggle { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 600; background: #fff; border: 1px solid var(--line); border-radius: 18px; padding: 5px 12px 5px 6px; cursor: pointer; user-select: none; } +.tg { width: 30px; height: 17px; border-radius: 10px; background: #c8d2da; position: relative; flex: none; transition: background 0.15s; } +.tg::after { content: ''; position: absolute; top: 2px; left: 2px; width: 13px; height: 13px; border-radius: 50%; background: #fff; transition: transform 0.15s; } +.toggle.on .tg { background: var(--orange); } +.toggle.on .tg::after { transform: translateX(13px); } + +/* ── ManCom ────────────────────────────────────────── */ +.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; margin-bottom: 14px; } +.kpi .lab { font-family: ui-monospace, monospace; font-size: 9px; letter-spacing: 1.3px; color: var(--muted); text-transform: uppercase; } +.kpi .val { font-size: 24px; font-weight: 800; margin: 4px 0 2px; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; } +.kpi .sub { font-size: 11px; color: var(--muted); } +.d-up { font-size: 12px; font-weight: 700; color: var(--ok); } +.mchip { font-size: 9.5px; font-weight: 700; border-radius: 10px; padding: 1px 8px; } +.mc-bad { background: #fdecec; color: var(--bad); border: 1px solid #f3c2c2; } +.mc-proj { background: #fff5f0; color: var(--orange); border: 1px dashed #e8a886; } + +.grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)); gap: 12px; margin-bottom: 14px; } +.mancom { display: grid; grid-template-columns: 1fr 290px; gap: 14px; align-items: start; } +@media (max-width: 1100px) { .mancom { grid-template-columns: 1fr; } } +.rail { display: flex; flex-direction: column; gap: 10px; } +.railh { font-family: ui-monospace, monospace; font-size: 10px; letter-spacing: 1.5px; color: var(--muted); text-transform: uppercase; display: flex; align-items: center; } +.railh a { margin-left: auto; font-size: 11px; } + +.ins { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; border-left-width: 4px; } +.ins.critical { border-left-color: var(--bad); } +.ins.warning { border-left-color: var(--warn); } +.ins.positive { border-left-color: var(--ok); } +.ins.info { border-left-color: var(--blue); } +.ins .sev { font-family: ui-monospace, monospace; font-size: 8.5px; letter-spacing: 1.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: ui-monospace, monospace; font-size: 8.5px; color: var(--teal); + background: #e9f5f6; border: 1px solid #c4e2e6; border-radius: 4px; padding: 1px 6px; +} +.gen { font-family: ui-monospace, monospace; font-size: 9px; color: var(--muted); margin-top: 7px; } + +.lgnd { display: flex; gap: 14px; font-size: 10px; color: var(--muted); margin-top: 6px; flex-wrap: wrap; } +.lgnd span { display: inline-flex; align-items: center; gap: 6px; } +.lk { width: 16px; height: 0; border-top: 2.6px solid; display: inline-block; } + +/* ── login ─────────────────────────────────────────── */ +.loginwrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; } +.login { width: 380px; } +.login .brand { display: flex; align-items: center; gap: 12px; margin-bottom: 22px; } +.login .slogo { width: 44px; height: 44px; font-size: 21px; border-radius: 10px; } +.login h1 { margin: 0; font-size: 19px; } +.login .sub { font-size: 11px; color: var(--muted); letter-spacing: 1px; text-transform: uppercase; } +.err { background: #fdecec; border: 1px solid #f3c2c2; color: var(--bad); border-radius: 7px; padding: 9px 12px; font-size: 12.5px; margin-bottom: 12px; } +.hint { border-left: 3px solid var(--teal); background: #f0f7f8; color: #0c5e75; border-radius: 0 6px 6px 0; padding: 9px 12px; font-size: 12px; margin-top: 14px; } + +/* ── chat ──────────────────────────────────────────── */ +.chat { display: flex; flex-direction: column; gap: 10px; max-width: 720px; } +.bub { border-radius: 12px; padding: 10px 14px; font-size: 13px; line-height: 1.5; max-width: 86%; white-space: pre-wrap; } +.bub.me { align-self: flex-end; background: var(--ink); color: #fff; border-bottom-right-radius: 4px; } +.bub.ai { align-self: flex-start; background: #fff; border: 1px solid var(--line); border-bottom-left-radius: 4px; } +.askrow { display: flex; gap: 8px; max-width: 720px; } +.askrow input { flex: 1; border: 1.4px solid var(--ink); border-radius: 22px; padding: 10px 16px; } +.askrow input:focus { outline: none; } + +.muted { color: var(--muted); } +.spread { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.spread .right { margin-left: auto; } +.mb { margin-bottom: 14px; } 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/package-lock.json b/package-lock.json index 7f701d0..ed4e949 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,24 @@ "typescript": "^5.6.0" } }, + "apps/web": { + "name": "@ubi/web", + "version": "0.1.0", + "dependencies": { + "@ubi/types": "*", + "react": "^18.3.1", + "react-dom": "^18.3.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", @@ -164,6 +182,23 @@ "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", @@ -179,254 +214,2305 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-identifier": { + "node_modules/@babel/compat-data": { "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==", + "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/@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==", + "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": "github", - "url": "https://github.com/sponsors/Borewit" + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "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==", + "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", - "optional": true, + "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": ">=0.1.90" + "node": ">=6.9.0" } }, - "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==", + "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": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@inquirer/checkbox": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", - "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "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": { - "@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" + "@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": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": ">=6.9.0" } }, - "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==", + "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": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" + "@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": ">=18" + "node": ">=6.9.0" }, "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@babel/core": "^7.0.0" } }, - "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==", + "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": { - "@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" + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" }, "engines": { - "node": ">=18" + "node": ">=6.9.0" }, "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@babel/core": "^7.0.0" } }, - "node_modules/@inquirer/editor": { - "version": "4.2.23", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", - "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "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": { - "@inquirer/core": "^10.3.2", - "@inquirer/external-editor": "^1.0.3", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" + "@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": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "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==", + "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": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" + "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" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "engines": { + "node": ">=6.9.0" } }, - "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==", + "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": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.0" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { - "node": ">=18" + "node": ">=6.9.0" }, "peerDependencies": { - "@types/node": ">=18" + "@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" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "engines": { + "node": ">=6.9.0" } }, - "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==", + "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": ">=18" + "node": ">=6.9.0" } }, - "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==", + "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": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { - "node": ">=18" + "node": ">=6.9.0" }, "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@babel/core": "^7.0.0" } }, - "node_modules/@inquirer/number": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", - "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "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": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { - "node": ">=18" + "node": ">=6.9.0" }, "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@babel/core": "^7.0.0" } }, - "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==", + "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": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" + "@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/@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" @@ -667,6 +2753,17 @@ "@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", @@ -954,33 +3051,519 @@ "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": "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/@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", - "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==", + "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, - "engines": { - "node": ">=14" - } + "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", @@ -1011,6 +3594,77 @@ "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", @@ -1142,6 +3796,13 @@ "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", @@ -1156,6 +3817,34 @@ "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", @@ -1177,6 +3866,13 @@ "@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", @@ -1195,6 +3891,31 @@ "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", @@ -1522,6 +4243,23 @@ "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", @@ -1529,6 +4267,55 @@ "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", @@ -1544,6 +4331,58 @@ "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", @@ -2007,6 +4846,16 @@ "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", @@ -2051,6 +4900,13 @@ "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", @@ -2069,6 +4925,20 @@ "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", @@ -2079,52 +4949,123 @@ "vary": "^1" }, "engines": { - "node": ">= 0.10" + "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": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "url": "https://github.com/sponsors/inspect-js" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "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": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" }, "engines": { - "node": ">= 8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/dayjs": { @@ -2204,6 +5145,24 @@ "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", @@ -2287,6 +5246,22 @@ "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", @@ -2333,6 +5308,75 @@ "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", @@ -2370,6 +5414,82 @@ "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", @@ -2446,6 +5566,36 @@ "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", @@ -2545,6 +5695,24 @@ ], "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", @@ -2563,6 +5731,39 @@ "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", @@ -2683,6 +5884,21 @@ "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", @@ -2692,6 +5908,57 @@ "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", @@ -2725,6 +5992,13 @@ "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", @@ -2738,6 +6012,24 @@ "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", @@ -2802,6 +6094,23 @@ "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", @@ -2821,6 +6130,19 @@ "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", @@ -2843,6 +6165,22 @@ "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", @@ -2918,6 +6256,13 @@ "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", @@ -2961,6 +6306,21 @@ "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", @@ -2970,18 +6330,319 @@ "node": ">= 0.10" } }, - "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==", + "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" - }, - "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", + "dependencies": { + "call-bound": "^1.0.3" + }, "engines": { "node": ">= 0.4" }, @@ -2989,30 +6650,53 @@ "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==", + "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-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==", + "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": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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-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", @@ -3042,6 +6726,52 @@ "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", @@ -3078,6 +6808,24 @@ "@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", @@ -3113,7 +6861,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -3139,6 +6886,19 @@ "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", @@ -3186,6 +6946,16 @@ "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", @@ -3229,6 +6999,16 @@ "safe-buffer": "^5.0.1" } }, + "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", @@ -3281,6 +7061,13 @@ "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", @@ -3323,6 +7110,13 @@ "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", @@ -3340,6 +7134,18 @@ "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", @@ -3555,6 +7361,25 @@ "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", @@ -3619,6 +7444,37 @@ "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", @@ -3680,6 +7536,24 @@ "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", @@ -3736,6 +7610,13 @@ "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", @@ -3901,6 +7782,35 @@ "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", @@ -3940,6 +7850,19 @@ "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", @@ -3978,28 +7901,95 @@ "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==", + "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-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": ">= 0.6" + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" } }, - "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==", + "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": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" }, "engines": { - "node": ">= 0.10" + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" } }, "node_modules/readable-stream": { @@ -4036,6 +8026,108 @@ "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", @@ -4055,6 +8147,28 @@ "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", @@ -4086,6 +8200,51 @@ "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", @@ -4111,6 +8270,26 @@ "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", @@ -4131,12 +8310,56 @@ ], "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", @@ -4228,6 +8451,16 @@ "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", @@ -4264,6 +8497,37 @@ "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", @@ -4395,6 +8659,16 @@ "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", @@ -4405,6 +8679,16 @@ "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", @@ -4460,6 +8744,20 @@ "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", @@ -4468,42 +8766,145 @@ "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==", + "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": { - "safe-buffer": "~5.2.0" + "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-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==", + "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": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=8" + "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": { @@ -4541,6 +8942,16 @@ "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", @@ -4570,6 +8981,19 @@ "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", @@ -4594,6 +9018,35 @@ "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", @@ -4719,6 +9172,23 @@ "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", @@ -4760,6 +9230,16 @@ "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", @@ -4797,6 +9277,19 @@ "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", @@ -4842,6 +9335,69 @@ "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", @@ -5091,12 +9647,88 @@ "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", @@ -5116,6 +9748,17 @@ "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", @@ -5185,13 +9828,119 @@ "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==", + "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": ">= 0.8" + "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": { @@ -5217,6 +9966,13 @@ "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", @@ -5323,6 +10079,18 @@ "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", @@ -5338,6 +10106,73 @@ "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", @@ -5359,6 +10194,346 @@ "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", @@ -5416,6 +10591,13 @@ "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", From d0ad94ad6cc6904700ec0c53a7ebb2f5b47b4556 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 16:04:01 +0000 Subject: [PATCH 12/68] feat(web/pages): login, ManCom, AI insights, Ask AI, doc tracking, approvals, engineering + dept read-surface pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the approved mockup design against live API data; verified end-to-end in headless Chromium (CEO journey + PE→PM→VPO chain). https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/pages/Approvals.tsx | 58 +++++++++ apps/web/src/pages/Ask.tsx | 78 ++++++++++++ apps/web/src/pages/Dept.tsx | 82 +++++++++++++ apps/web/src/pages/Docs.tsx | 104 ++++++++++++++++ apps/web/src/pages/Engineering.tsx | 106 ++++++++++++++++ apps/web/src/pages/Insights.tsx | 60 ++++++++++ apps/web/src/pages/Login.tsx | 67 +++++++++++ apps/web/src/pages/ManCom.tsx | 186 +++++++++++++++++++++++++++++ 8 files changed, 741 insertions(+) create mode 100644 apps/web/src/pages/Approvals.tsx create mode 100644 apps/web/src/pages/Ask.tsx create mode 100644 apps/web/src/pages/Dept.tsx create mode 100644 apps/web/src/pages/Docs.tsx create mode 100644 apps/web/src/pages/Engineering.tsx create mode 100644 apps/web/src/pages/Insights.tsx create mode 100644 apps/web/src/pages/Login.tsx create mode 100644 apps/web/src/pages/ManCom.tsx diff --git a/apps/web/src/pages/Approvals.tsx b/apps/web/src/pages/Approvals.tsx new file mode 100644 index 0000000..330f3b3 --- /dev/null +++ b/apps/web/src/pages/Approvals.tsx @@ -0,0 +1,58 @@ +import { useEffect, useState } from 'react'; +import { api } from '../api'; + +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
Loading approvals…
; + + return ( + <> +

Approvals

+
SHARED ENGINE · PE → PM → VPO
+
+ {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.step.toUpperCase()} {s.status === 'approved' ? '✓' : i === a.currentStep ? '· you' : ''} + + ))} +
+
+ + +
+
+ ))} + + ); +} diff --git a/apps/web/src/pages/Ask.tsx b/apps/web/src/pages/Ask.tsx new file mode 100644 index 0000000..a8539c8 --- /dev/null +++ b/apps/web/src/pages/Ask.tsx @@ -0,0 +1,78 @@ +import { FormEvent, useState } from 'react'; +import type { AskAnswer } from '@ubi/types'; +import { api, getUser } from '../api'; + +interface Turn { + who: 'me' | 'ai'; + text: string; + sources?: string[]; +} + +export default function Ask() { + const [turns, setTurns] = useState([]); + const [q, setQ] = useState(''); + const [busy, setBusy] = useState(false); + const user = getUser(); + + async function send(question: string) { + if (!question.trim() || busy) return; + setTurns((t) => [...t, { who: 'me', text: question }]); + 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 }]); + } catch (err) { + setTurns((t) => [...t, { who: 'ai', text: `error: ${err instanceof Error ? err.message : 'failed'}` }]); + } finally { + setBusy(false); + } + } + + function submit(e: FormEvent) { + e.preventDefault(); + void send(q); + } + + return ( + <> +
+

Ask AI

+
+ scope: {user?.claims.join(' · ')} · clinic aggregate-only +
+
+
+ {turns.length === 0 && ( +
+ Ask across the whole suite — answers cite the read-models they came from (Pillar 3). +
+ )} + {turns.map((t, i) => ( +
+ {t.text} + {t.sources && ( +
+ {t.sources.map((s) => {s})} +
+ )} +
+ ))} + {busy &&
thinking…
} +
+
+ {["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…" + /> + +
+ + ); +} diff --git a/apps/web/src/pages/Dept.tsx b/apps/web/src/pages/Dept.tsx new file mode 100644 index 0000000..c1c2c87 --- /dev/null +++ b/apps/web/src/pages/Dept.tsx @@ -0,0 +1,82 @@ +import { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { api } from '../api'; + +/** Breadth-first department pages: render the module's read surface as-is. + * Deep workflows land per module branch (see CLAUDE.md §11). */ +const endpoints: Record = { + procurement: { title: 'Procurement — supplier performance', path: '/procurement/supplier-performance', src: 'procurement.supplier-performance' }, + operations: { title: 'Operations — VPO control tower', path: '/operations/health', src: 'operations.health' }, + survey: { title: 'Survey — monthly volumes', path: '/survey/volumes', src: 'survey.volumes' }, + mqc: { title: 'MQC — certificate queue', path: '/mqc/certs', src: 'mqc.certs' }, + audit: { title: 'Audit — exception register', path: '/audit/exceptions', src: 'audit.exceptions' }, + it: { title: 'IT — capacity planning & controls', path: '/it/capacity', src: 'it.capacity' }, + records: { title: 'Records — expiring documents', path: '/records/expiry', src: 'records.expiry' }, + clinic: { title: 'Clinic — aggregate summary (isolated)', path: '/clinic/aggregate', src: 'clinic.aggregate' }, + admin: { title: 'Admin / OHS — inquiries (shared ticketing)', path: '/tickets?source=admin', src: 'admin.tickets' }, + hr: { title: 'HR — hours computation runs', path: '/hr/hours', src: 'hr.hours' }, + property: { title: 'Property — asset custody (QR EAM)', path: '/property/assets', src: 'property.assets' }, + finance: { title: 'Finance — documents in motion', path: '/finance/doc-flow', src: 'finance.doc-flow' }, +}; + +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}
+ ); +} + +export default function Dept() { + const { slug = '' } = useParams(); + const meta = endpoints[slug]; + const [data, setData] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + setData(null); + setError(null); + if (meta) api(meta.path).then(setData).catch((e: Error) => setError(e.message)); + }, [slug]); + + if (!meta) return
Unknown department: {slug}
; + + return ( + <> +

{meta.title}

+
SRC · {meta.src}
+
+ {error &&
{error}
} + {!error && data == null &&
Loading…
} + {Array.isArray(data) &&
>} />
} + {!Array.isArray(data) && data != null && typeof data === 'object' && ( +
+ + + {Object.entries(data as Record).map(([k, v]) => ( + + + + + ))} + +
{k}{Array.isArray(v) ? >} /> : }
+
+ )} + + ); +} diff --git a/apps/web/src/pages/Docs.tsx b/apps/web/src/pages/Docs.tsx new file mode 100644 index 0000000..25463ab --- /dev/null +++ b/apps/web/src/pages/Docs.tsx @@ -0,0 +1,104 @@ +import { FormEvent, useEffect, useState } from 'react'; +import type { TrackedDocument, WhereIsAnswer } from '@ubi/types'; +import { api } from '../api'; + +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 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 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 ( + <> +

Document Tracking

+
PILLAR 2 · BACKBONE
+
+ +
+ setCode(e.target.value)} placeholder="Where is… (scan or type a code, e.g. DOC-2026-00001)" /> + +
+ {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..6e52021 --- /dev/null +++ b/apps/web/src/pages/Engineering.tsx @@ -0,0 +1,106 @@ +import { useEffect, useState } from 'react'; +import type { MaterialsSchedule, PayItem, Project } from '@ubi/types'; +import { api, getUser, hasClaim } from '../api'; + +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 ( + <> +

Engineering

+
SRC · engineering.projects / swa
+
+ {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/Insights.tsx b/apps/web/src/pages/Insights.tsx new file mode 100644 index 0000000..c0e6c4d --- /dev/null +++ b/apps/web/src/pages/Insights.tsx @@ -0,0 +1,60 @@ +import { useEffect, useState } from 'react'; +import type { Insight } from '@ubi/types'; +import { api } from '../api'; + +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
Loading insights…
; + const shown = items.filter((i) => filter === 'all' || i.severity === filter); + const counts = (sev: string) => items.filter((i) => i.severity === sev).length; + + return ( + <> +
+

AI Insights

+
+ generated off-box · posted via /api/ai/insights +
+
+
+ {(['all', 'critical', 'warning', 'positive'] as const).map((f) => ( + + ))} +
+ {shown.map((ins) => ( +
+
+ + {ins.severity.toUpperCase()} + + {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/Login.tsx b/apps/web/src/pages/Login.tsx new file mode 100644 index 0000000..8ca6ead --- /dev/null +++ b/apps/web/src/pages/Login.tsx @@ -0,0 +1,67 @@ +import { FormEvent, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import type { UserClaims } from '@ubi/types'; +import { api, setSession } from '../api'; + +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(); + + 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); + const executive = res.user.claims.some((c) => ['role:ceo', 'role:vpo', 'role:admin'].includes(c)); + navigate(executive ? '/mancom' : '/approvals'); + } catch (err) { + setError(err instanceof Error ? err.message : 'login failed'); + } finally { + setBusy(false); + } + } + + return ( +
+
+
+
U
+
+

UBI Construction Suite

+
Ulticon Builders · Omega Asia
+
+
+ {error &&
{error}
} +
+ + + + +
+
+ Offline-ready: after the first online sign-in this device keeps a secure session for field + use. Demo logins: ceo@ubi.ph/ceo123 · vpo@ubi.ph/vpo123 ·{' '} + pm@ubi.ph/pm123 · pe@ubi.ph/pe123 +
+
+
+ ); +} diff --git a/apps/web/src/pages/ManCom.tsx b/apps/web/src/pages/ManCom.tsx new file mode 100644 index 0000000..66788f8 --- /dev/null +++ b/apps/web/src/pages/ManCom.tsx @@ -0,0 +1,186 @@ +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'; + +const peso = (centavos: number) => `₱${(centavos / 100 / 1_000_000).toFixed(1)}M`; + +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
Loading ManCom…
; + const k = snap.kpis; + + return ( + <> +
+

ManCom

+
+ + 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}% +
+
+ {k.billingDraftsSubmitted} of {k.billingDraftsTotal} monthly drafts submitted +
+
+
+
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}% + + ) : ( + + )} +
+
+ +
+
+ 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?” → + +
+
+
+ + ); +} From 0796e412d641450711c080d7e402901563fc246b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 16:04:01 +0000 Subject: [PATCH 13/68] docs(readme): status + run-locally instructions with demo logins https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- README.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8b857ed..0d327ba 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,24 @@ covering everything outside the Acumatica ERP. This repo currently holds the `__`. ## Status -Pre-code. Next: monorepo scaffold (API + PWA + Postgres + StorageProvider + -the three pillars stubbed), built on the module branches per the phasing. +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**. From 4f76ee7a69655646db73a27e2e1fb62b4fa0f219 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 16:18:36 +0000 Subject: [PATCH 14/68] =?UTF-8?q?feat(web/design):=20'drawing=20comes=20al?= =?UTF-8?q?ive'=20design=20system=20=E2=80=94=20drafting=20grid,=20sheet?= =?UTF-8?q?=20cards=20w/=20crop=20marks,=20title=20blocks,=20stamps,=20cha?= =?UTF-8?q?inage=20rulers,=20hazard=20stripes;=20Space=20Grotesk=20+=20IBM?= =?UTF-8?q?=20Plex=20Mono=20bundled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/package.json | 2 + apps/web/src/main.tsx | 4 + apps/web/src/styles.css | 370 +++++++++++++++++++++++++++------------- apps/web/src/ui.tsx | 62 +++++++ package-lock.json | 20 +++ 5 files changed, 339 insertions(+), 119 deletions(-) create mode 100644 apps/web/src/ui.tsx diff --git a/apps/web/package.json b/apps/web/package.json index d956bfa..ca93686 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "@fontsource-variable/space-grotesk": "^5.2.10", + "@fontsource/ibm-plex-mono": "^5.2.7", "@ubi/types": "*", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 3520189..59da7f4 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -2,6 +2,10 @@ 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/styles.css b/apps/web/src/styles.css index 05f2492..fb6ce17 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -1,23 +1,38 @@ -/* UBI Suite — brand light UI (per approved mockups.html) - navy chrome · orange action · teal data · blue AI */ +/* ═══════════════════════════════════════════════════════════════════ + 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: #0f2233; + --ink: #102536; + --ink2: #23476b; --orange: #c2410c; + --orange-d: #9a3409; --teal: #0e7490; --blue: #1d4ed8; --ok: #15803d; --warn: #b45309; --bad: #b91c1c; --muted: #5b6b7a; - --bg: #f3f5f7; - --line: #e3e8ee; + --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: var(--bg); + 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: 'Inter', 'Segoe UI', system-ui, sans-serif; + font-family: var(--disp); font-size: 14px; line-height: 1.5; -webkit-font-smoothing: antialiased; @@ -25,123 +40,218 @@ body { button { font: inherit; cursor: pointer; } input, select { font: inherit; } a { color: var(--blue); text-decoration: none; } -code { font-family: ui-monospace, Menlo, monospace; font-size: 0.92em; } +code { font-family: var(--mono); font-size: 0.9em; letter-spacing: -0.2px; } +::selection { background: var(--orange); color: #fff; } -/* ── shell ─────────────────────────────────────────── */ +/* ── shell ─────────────────────────────────────────────────────── */ .shell { display: flex; min-height: 100vh; } .side { - flex: 0 0 216px; background: var(--ink); color: #cfdae4; - display: flex; flex-direction: column; padding: 14px 0 12px; - position: sticky; top: 0; height: 100vh; overflow-y: auto; + 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: 9px; padding: 2px 16px 14px; border-bottom: 1px solid #1d3a54; } +.sbrand { display: flex; align-items: center; gap: 10px; padding: 2px 16px 14px; border-bottom: 1px dashed #2c4d6c; } .slogo { - width: 30px; height: 30px; border-radius: 7px; background: var(--orange); color: #fff; - display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 15px; + 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: 14px; line-height: 1.15; } -.sbrand span { font-size: 9.5px; color: #8fa6b8; letter-spacing: 0.8px; text-transform: uppercase; } +.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: #1c3954; border: 1px solid #2c4d6c; border-radius: 6px; - padding: 7px 10px; font-size: 11.5px; color: #dbe7f1; display: flex; justify-content: space-between; + 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: #fff; } -.sgrp { padding: 13px 16px 5px; font-family: ui-monospace, monospace; font-size: 9px; letter-spacing: 1.6px; color: #7e96aa; 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 { - display: block; padding: 7px 16px; font-size: 13px; color: #cfdae4; - border-left: 3px solid transparent; -} -.sitem:hover { background: #16314a; color: #fff; } -.sitem.on { background: #1c3954; color: #fff; border-left-color: var(--orange); font-weight: 600; } -.sitem.dim { color: #8fa6b8; font-size: 12.5px; padding: 5.5px 16px; } -.suser { margin-top: auto; border-top: 1px solid #1d3a54; padding: 11px 16px; display: flex; gap: 9px; align-items: center; } -.suser .av { width: 28px; height: 28px; border-radius: 50%; background: #2c4d6c; color: #fff; font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex: none; } + 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-size: 9.5px; color: #8fa6b8; } -.suser button { margin-left: auto; background: none; border: 1px solid #2c4d6c; color: #8fa6b8; border-radius: 5px; font-size: 10.5px; padding: 3px 8px; } -.suser button:hover { color: #fff; border-color: #8fa6b8; } +.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; } -.main { flex: 1; min-width: 0; padding: 20px 24px 32px; } -.top { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 16px; } -.top h1 { margin: 0; font-size: 21px; font-weight: 800; } +.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-size: 11.5px; color: var(--muted); display: flex; align-items: center; gap: 6px; } -.asof i { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); } +.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); } -/* ── components ────────────────────────────────────── */ -.btn { - border: 1px solid var(--line); background: #fff; color: var(--ink); - border-radius: 7px; padding: 7px 14px; font-weight: 600; font-size: 13px; -} -.btn:hover { border-color: var(--muted); } -.btn.pri { background: var(--orange); border-color: var(--orange); color: #fff; } -.btn.pri:hover { background: #a83709; } -.btn.navy { background: var(--ink); border-color: var(--ink); color: #fff; } -.btn.ok { background: var(--ok); border-color: var(--ok); color: #fff; } -.btn.sm { padding: 4px 10px; font-size: 11.5px; } -.btn:disabled { opacity: 0.5; cursor: default; } - -.card { background: #fff; border: 1px solid var(--line); border-radius: 9px; padding: 14px 16px; } -.ph { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; } -.ph b { font-size: 13.5px; } +/* ── 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: ui-monospace, monospace; font-size: 9px; letter-spacing: 0.6px; - color: var(--blue); background: #eef3ff; border: 1px solid #ccdaf8; border-radius: 4px; - padding: 1px 7px; white-space: nowrap; + 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; } -.st { font-size: 10px; font-weight: 700; border-radius: 10px; padding: 2px 9px; white-space: nowrap; } -.st-ok { background: #e9f6ee; color: var(--ok); border: 1px solid #c4e5d1; } -.st-warn { background: #fdf3e7; color: var(--warn); border: 1px solid #f0d7b5; } -.st-bad { background: #fdecec; color: var(--bad); border: 1px solid #f3c2c2; } -.st-info { background: #eef3ff; color: var(--blue); border: 1px solid #ccdaf8; } +/* ── 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; } -.tbl { width: 100%; border-collapse: collapse; font-size: 12.5px; } -.tbl th { - font-family: ui-monospace, monospace; font-size: 9.5px; letter-spacing: 1px; text-transform: uppercase; - color: var(--muted); text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--line); font-weight: 600; +.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; } -.tbl td { padding: 7px 8px; border-bottom: 1px solid #eef2f5; vertical-align: middle; } -.tbl tr:last-child td { border-bottom: 0; } +.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; } -.field { display: block; margin-bottom: 12px; } -.field span { display: block; font-size: 11px; font-weight: 600; color: var(--muted); margin-bottom: 4px; letter-spacing: 0.3px; } -.field input, .field select { - width: 100%; border: 1.4px solid var(--line); border-radius: 7px; padding: 9px 12px; font-size: 14px; +/* ── 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); } -.field input:focus { outline: none; border-color: var(--ink); } - -.toggle { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 600; background: #fff; border: 1px solid var(--line); border-radius: 18px; padding: 5px 12px 5px 6px; cursor: pointer; user-select: none; } -.tg { width: 30px; height: 17px; border-radius: 10px; background: #c8d2da; position: relative; flex: none; transition: background 0.15s; } -.tg::after { content: ''; position: absolute; top: 2px; left: 2px; width: 13px; height: 13px; border-radius: 50%; background: #fff; transition: transform 0.15s; } -.toggle.on .tg { background: var(--orange); } -.toggle.on .tg::after { transform: translateX(13px); } +.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); } -/* ── ManCom ────────────────────────────────────────── */ -.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; margin-bottom: 14px; } -.kpi .lab { font-family: ui-monospace, monospace; font-size: 9px; letter-spacing: 1.3px; color: var(--muted); text-transform: uppercase; } -.kpi .val { font-size: 24px; font-weight: 800; margin: 4px 0 2px; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; } +/* ── 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-size: 12px; font-weight: 700; color: var(--ok); } -.mchip { font-size: 9.5px; font-weight: 700; border-radius: 10px; padding: 1px 8px; } -.mc-bad { background: #fdecec; color: var(--bad); border: 1px solid #f3c2c2; } -.mc-proj { background: #fff5f0; color: var(--orange); border: 1px dashed #e8a886; } +.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; } -.grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)); gap: 12px; margin-bottom: 14px; } -.mancom { display: grid; grid-template-columns: 1fr 290px; gap: 14px; align-items: start; } +/* ── 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: 10px; } -.railh { font-family: ui-monospace, monospace; font-size: 10px; letter-spacing: 1.5px; color: var(--muted); text-transform: uppercase; display: flex; align-items: center; } -.railh a { margin-left: auto; font-size: 11px; } - -.ins { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; border-left-width: 4px; } -.ins.critical { border-left-color: var(--bad); } -.ins.warning { border-left-color: var(--warn); } -.ins.positive { border-left-color: var(--ok); } -.ins.info { border-left-color: var(--blue); } -.ins .sev { font-family: ui-monospace, monospace; font-size: 8.5px; letter-spacing: 1.2px; font-weight: 700; } +.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); } @@ -149,33 +259,55 @@ code { font-family: ui-monospace, Menlo, monospace; font-size: 0.92em; } .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: ui-monospace, monospace; font-size: 8.5px; color: var(--teal); - background: #e9f5f6; border: 1px solid #c4e2e6; border-radius: 4px; padding: 1px 6px; + 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: ui-monospace, monospace; font-size: 9px; color: var(--muted); margin-top: 7px; } +.gen { font-family: var(--mono); font-size: 8.5px; color: var(--muted); margin-top: 8px; letter-spacing: 0.4px; } -.lgnd { display: flex; gap: 14px; font-size: 10px; color: var(--muted); margin-top: 6px; flex-wrap: wrap; } +/* ── 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 ─────────────────────────────────────────── */ +/* ── login: a drafting sheet ───────────────────────────────────── */ .loginwrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; } -.login { width: 380px; } -.login .brand { display: flex; align-items: center; gap: 12px; margin-bottom: 22px; } -.login .slogo { width: 44px; height: 44px; font-size: 21px; border-radius: 10px; } -.login h1 { margin: 0; font-size: 19px; } -.login .sub { font-size: 11px; color: var(--muted); letter-spacing: 1px; text-transform: uppercase; } -.err { background: #fdecec; border: 1px solid #f3c2c2; color: var(--bad); border-radius: 7px; padding: 9px 12px; font-size: 12.5px; margin-bottom: 12px; } -.hint { border-left: 3px solid var(--teal); background: #f0f7f8; color: #0c5e75; border-radius: 0 6px 6px 0; padding: 9px 12px; font-size: 12px; margin-top: 14px; } - -/* ── chat ──────────────────────────────────────────── */ -.chat { display: flex; flex-direction: column; gap: 10px; max-width: 720px; } -.bub { border-radius: 12px; padding: 10px 14px; font-size: 13px; line-height: 1.5; max-width: 86%; white-space: pre-wrap; } -.bub.me { align-self: flex-end; background: var(--ink); color: #fff; border-bottom-right-radius: 4px; } -.bub.ai { align-self: flex-start; background: #fff; border: 1px solid var(--line); border-bottom-left-radius: 4px; } -.askrow { display: flex; gap: 8px; max-width: 720px; } -.askrow input { flex: 1; border: 1.4px solid var(--ink); border-radius: 22px; padding: 10px 16px; } -.askrow input:focus { outline: none; } +.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; } + +/* ── 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; } .muted { color: var(--muted); } .spread { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } diff --git a/apps/web/src/ui.tsx b/apps/web/src/ui.tsx new file mode 100644 index 0000000..5e07f73 --- /dev/null +++ b/apps/web/src/ui.tsx @@ -0,0 +1,62 @@ +/** Signature components of the "drawing comes alive" design language. */ + +/** 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 + + ); +} + +/** 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/package-lock.json b/package-lock.json index ed4e949..a4ef96b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,8 @@ "name": "@ubi/web", "version": "0.1.0", "dependencies": { + "@fontsource-variable/space-grotesk": "^5.2.10", + "@fontsource/ibm-plex-mono": "^5.2.7", "@ubi/types": "*", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -2296,6 +2298,24 @@ "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", From 2225f9bf939febe89eb7701d5be9e20569a3e215 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 16:18:36 +0000 Subject: [PATCH 15/68] =?UTF-8?q?feat(web/design):=20apply=20design=20lang?= =?UTF-8?q?uage=20across=20pages=20=E2=80=94=20SheetBar=20headers,=20stamp?= =?UTF-8?q?ed=20severities/approvals,=20billing=20chainage,=20survey-flag?= =?UTF-8?q?=20chart=20marker,=20drafting-sheet=20login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/charts.tsx | 11 ++++-- apps/web/src/pages/Approvals.tsx | 21 +++++++----- apps/web/src/pages/Ask.tsx | 8 ++--- apps/web/src/pages/Dept.tsx | 5 ++- apps/web/src/pages/Docs.tsx | 5 ++- apps/web/src/pages/Engineering.tsx | 5 ++- apps/web/src/pages/Insights.tsx | 16 ++++----- apps/web/src/pages/Login.tsx | 55 +++++++++++++++++------------- apps/web/src/pages/ManCom.tsx | 24 +++++++------ 9 files changed, 80 insertions(+), 70 deletions(-) diff --git a/apps/web/src/charts.tsx b/apps/web/src/charts.tsx index 57cb6d4..9756f0b 100644 --- a/apps/web/src/charts.tsx +++ b/apps/web/src/charts.tsx @@ -37,8 +37,15 @@ export function TrendChart({ data, project, unit }: { data: TrendPoint[]; projec {Math.round(max)} {lastActualIdx >= 0 && ( <> - - + + {/* survey flag marks the station we've reached */} + + TODAY diff --git a/apps/web/src/pages/Approvals.tsx b/apps/web/src/pages/Approvals.tsx index 330f3b3..e56a645 100644 --- a/apps/web/src/pages/Approvals.tsx +++ b/apps/web/src/pages/Approvals.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import { api } from '../api'; +import { SheetBar, Stamp } from '../ui'; interface ApprovalRow { id: string; @@ -29,9 +30,7 @@ export default function Approvals() { return ( <> -

Approvals

-
SHARED ENGINE · PE → PM → VPO
-
+ {rows.length === 0 &&
Inbox zero — nothing waits on you.
} {rows.map((a) => (
@@ -40,12 +39,16 @@ export default function Approvals() { {a.title} by {a.createdBy} · {new Date(a.createdAt).toLocaleDateString()}
-
- {a.chain.map((s, i) => ( - - {s.step.toUpperCase()} {s.status === 'approved' ? '✓' : i === a.currentStep ? '· you' : ''} - - ))} +
+ {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 index a8539c8..73b49a2 100644 --- a/apps/web/src/pages/Ask.tsx +++ b/apps/web/src/pages/Ask.tsx @@ -1,6 +1,7 @@ import { FormEvent, useState } from 'react'; import type { AskAnswer } from '@ubi/types'; import { api, getUser } from '../api'; +import { SheetBar } from '../ui'; interface Turn { who: 'me' | 'ai'; @@ -36,12 +37,7 @@ export default function Ask() { return ( <> -
-

Ask AI

-
- scope: {user?.claims.join(' · ')} · clinic aggregate-only -
-
+
{turns.length === 0 && (
diff --git a/apps/web/src/pages/Dept.tsx b/apps/web/src/pages/Dept.tsx index c1c2c87..4999b84 100644 --- a/apps/web/src/pages/Dept.tsx +++ b/apps/web/src/pages/Dept.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { api } from '../api'; +import { SheetBar } from '../ui'; /** Breadth-first department pages: render the module's read surface as-is. * Deep workflows land per module branch (see CLAUDE.md §11). */ @@ -57,9 +58,7 @@ export default function Dept() { return ( <> -

{meta.title}

-
SRC · {meta.src}
-
+ {error &&
{error}
} {!error && data == null &&
Loading…
} {Array.isArray(data) &&
>} />
} diff --git a/apps/web/src/pages/Docs.tsx b/apps/web/src/pages/Docs.tsx index 25463ab..2c8a5b0 100644 --- a/apps/web/src/pages/Docs.tsx +++ b/apps/web/src/pages/Docs.tsx @@ -1,6 +1,7 @@ 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([]); @@ -44,9 +45,7 @@ export default function Docs() { return ( <> -

Document Tracking

-
PILLAR 2 · BACKBONE
-
+
setCode(e.target.value)} placeholder="Where is… (scan or type a code, e.g. DOC-2026-00001)" /> diff --git a/apps/web/src/pages/Engineering.tsx b/apps/web/src/pages/Engineering.tsx index 6e52021..9eadb9f 100644 --- a/apps/web/src/pages/Engineering.tsx +++ b/apps/web/src/pages/Engineering.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; 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([]); @@ -41,9 +42,7 @@ export default function Engineering() { return ( <> -

Engineering

-
SRC · engineering.projects / swa
-
+ {error &&
{error}
}
diff --git a/apps/web/src/pages/Insights.tsx b/apps/web/src/pages/Insights.tsx index c0e6c4d..5ca4360 100644 --- a/apps/web/src/pages/Insights.tsx +++ b/apps/web/src/pages/Insights.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import type { Insight } from '@ubi/types'; import { api } from '../api'; +import { SheetBar, Stamp } from '../ui'; export default function Insights() { const [items, setItems] = useState(null); @@ -18,12 +19,7 @@ export default function Insights() { return ( <> -
-

AI Insights

-
- generated off-box · posted via /api/ai/insights -
-
+
{(['all', 'critical', 'warning', 'positive'] as const).map((f) => ( + + +
+
SheetLOGIN-01
+
Drawn byUBI / IT
+
ScaleNTS
+
RevA
- {error &&
{error}
} -
- - - - -
Offline-ready: after the first online sign-in this device keeps a secure session for field - use. Demo logins: ceo@ubi.ph/ceo123 · vpo@ubi.ph/vpo123 ·{' '} + use. Demo: ceo@ubi.ph/ceo123 · vpo@ubi.ph/vpo123 ·{' '} pm@ubi.ph/pm123 · pe@ubi.ph/pe123
diff --git a/apps/web/src/pages/ManCom.tsx b/apps/web/src/pages/ManCom.tsx index 66788f8..d28f2f0 100644 --- a/apps/web/src/pages/ManCom.tsx +++ b/apps/web/src/pages/ManCom.tsx @@ -3,6 +3,7 @@ import { Link } from 'react-router-dom'; import type { ManComSnapshot } from '@ubi/types'; import { api } from '../api'; import { BarsChart, Sparkline, TrendChart } from '../charts'; +import { Chainage, SheetBar } from '../ui'; const peso = (centavos: number) => `₱${(centavos / 100 / 1_000_000).toFixed(1)}M`; @@ -28,12 +29,12 @@ export default function ManCom() { return ( <> +
-

ManCom

+ + Project trends forward +
- - Project trends forward - live · as of {new Date(snap.asOf).toLocaleTimeString()} @@ -43,23 +44,24 @@ export default function ManCom() {
-
+
Active projects
{k.activeProjects} {k.flaggedProjects} ⚑
DPWH packages · flags pre-vetted by Operations
-
+
Billings — month
{peso(k.billingsMonthCentavos)} ▲ {k.billingsDeltaPct}%
-
- {k.billingDraftsSubmitted} of {k.billingDraftsTotal} monthly drafts submitted -
+
-
+
Avg slippage
{k.avgSlippagePct}% @@ -69,7 +71,7 @@ export default function ManCom() {
across active packages
-
+
Open exceptions
{k.openExceptions} {k.escalatedExceptions} escalated From 2b0819c4dccda8c79e67cd7a0d2999019df4c0be Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 02:15:38 +0000 Subject: [PATCH 16/68] =?UTF-8?q?docs(claude-md):=20add=20=C2=A70=20curren?= =?UTF-8?q?t-build-state=20=E2=80=94=20handoff=20for=20new=20Claude=20sess?= =?UTF-8?q?ions=20(branches,=20run,=20prod=20env,=20next=20work)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- CLAUDE.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index b315eae..c89c43d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,6 +8,55 @@ --- +## 0. CURRENT BUILD STATE — START HERE (updated 2026-06-12) + +> **For Claude Code sessions:** the suite is **no longer pre-code**. A working +> breadth-first build lives on branch **`kickoff-build`** (PR #1 → `main`). +> If you cloned `main` and see only docs, run `git checkout kickoff-build` +> first. A deployment is live at `edge.ubi-as.com`. + +**What exists and runs** +- Monorepo: `apps/api` (NestJS), `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 — 15 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). +- **Modules:** Engineering core (projects, DPWH pay-item library, AI-prefilled + weekly materials schedule → approvals), Procurement DR capture, Operations + control tower, CEO ManCom consolidation (`/api/ceo/mancom`); every other + department exposes a read-surface stub registered in the AI layer. +- **Web:** "drawing comes alive" design system (title-block headers, stamps, + chainage rulers — `mockups.html` is the approved reference). Pages: login, + ManCom, AI Insights, Ask AI, Documents, Approvals, Engineering, + per-department read surfaces. + +**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`. If the live DB was seeded with demo +users, **rotate or disable them immediately** — the demo passwords are +printed in this repo. + +**Next work, in order** +1. Finish production hardening: TypeORM migrations to replace `DB_SYNC`, + helmet, nginx/systemd/deploy docs, Postgres + uploads backups. +2. Real SWA/billing tables behind `engineering.swa`; field photo capture + pipeline (GPS + timestamp + <500 KB compress + offline queue); Google + OAuth; offline cached login. +3. Deep module work on its own branch per §11 (`survey`, `mqc`, `it`, …), + keeping each module's Pillar-3 read model as the contract. + +Micro-commits, conventional scopes per §11. + +--- + ## 1. What this is A **full construction management suite for Ulticon Builders, Inc. (UBI)** that From 33a70ac63e2275255aeb1e4cebb76cef177925d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 02:15:38 +0000 Subject: [PATCH 17/68] feat(api/security): gate demo seed behind SEED_DEMO, require JWT_SECRET in production, pin CORS via CORS_ORIGIN, throttle /auth/login (10/min/IP) Verified: 11th bad login returns 429; boot fails on dev JWT default when NODE_ENV=production. https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- .env.example | 5 +++ apps/api/src/main.ts | 4 ++- apps/api/src/pillars/auth/auth.controller.ts | 34 ++++++++++++++++++-- apps/api/src/pillars/auth/auth.module.ts | 11 +++++-- apps/api/src/seed/seed.module.ts | 7 ++++ 5 files changed, 55 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 2dab9ef..ab9369a 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,12 @@ # ── 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 diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 2957c79..fb62b33 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -7,7 +7,9 @@ async function bootstrap(): Promise { const app = await NestFactory.create(AppModule); app.setGlobalPrefix('api'); app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); - app.enableCors({ origin: true, credentials: 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 diff --git a/apps/api/src/pillars/auth/auth.controller.ts b/apps/api/src/pillars/auth/auth.controller.ts index d90ed78..356b9db 100644 --- a/apps/api/src/pillars/auth/auth.controller.ts +++ b/apps/api/src/pillars/auth/auth.controller.ts @@ -1,10 +1,39 @@ -import { Body, Controller, Get, HttpCode, NotImplementedException, Post } from '@nestjs/common'; +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; @@ -24,7 +53,8 @@ export class AuthController { @Public() @HttpCode(200) @Post('login') - login(@Body() dto: LoginDto): Promise { + login(@Req() req: Request, @Body() dto: LoginDto): Promise { + throttle(req.ip ?? req.socket.remoteAddress ?? 'unknown'); return this.auth.login(dto.email, dto.password); } diff --git a/apps/api/src/pillars/auth/auth.module.ts b/apps/api/src/pillars/auth/auth.module.ts index 8dc7b65..bc8686b 100644 --- a/apps/api/src/pillars/auth/auth.module.ts +++ b/apps/api/src/pillars/auth/auth.module.ts @@ -13,9 +13,14 @@ import { UserEntity } from './user.entity'; JwtModule.registerAsync({ global: true, inject: [ConfigService], - useFactory: (config: ConfigService) => ({ - secret: config.get('JWT_SECRET', 'dev-only-secret'), - }), + 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], diff --git a/apps/api/src/seed/seed.module.ts b/apps/api/src/seed/seed.module.ts index 25c81d8..84a128e 100644 --- a/apps/api/src/seed/seed.module.ts +++ b/apps/api/src/seed/seed.module.ts @@ -1,4 +1,5 @@ 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'; @@ -16,6 +17,7 @@ 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, @@ -25,6 +27,11 @@ export class SeedService implements OnApplicationBootstrap { ) {} 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; + } if ((await this.payItems.count()) > 0) return; this.log.log('empty database — seeding demo data'); From 23e9228eda34e6f507314578f85dcf4249d7f148 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 09:37:44 +0000 Subject: [PATCH 18/68] =?UTF-8?q?feat(it/helpdesk):=20public=20no-signin?= =?UTF-8?q?=20ticket=20intake=20=E2=80=94=20Cloudflare=20Turnstile=20serve?= =?UTF-8?q?r-side=20verify=20(enforced=20when=20TURNSTILE=5FSECRET=5FKEY?= =?UTF-8?q?=20set),=205/min/IP=20spam=20throttle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- .env.example | 6 + .../src/shared/ticketing/ticketing.module.ts | 105 +++++++++++++++++- 2 files changed, 107 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index ab9369a..7f76f86 100644 --- a/.env.example +++ b/.env.example @@ -33,6 +33,12 @@ STORAGE_ROOT=./uploads 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= diff --git a/apps/api/src/shared/ticketing/ticketing.module.ts b/apps/api/src/shared/ticketing/ticketing.module.ts index 044c239..e34d3a7 100644 --- a/apps/api/src/shared/ticketing/ticketing.module.ts +++ b/apps/api/src/shared/ticketing/ticketing.module.ts @@ -1,10 +1,27 @@ -import { Body, Controller, Get, Global, Injectable, Module, Param, Post, Query } from '@nestjs/common'; +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 { IsIn, IsOptional, IsString, MinLength } from 'class-validator'; +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 } from '../../common/rbac'; +import { CurrentUser, Public } from '../../common/rbac'; /** One ticketing engine shared by IT helpdesk AND Admin inquiries. */ @Entity('ticket') @@ -89,10 +106,90 @@ export class TicketingController { } } +/* ── 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], + controllers: [TicketingController, PublicHelpdeskController], providers: [TicketingService], exports: [TicketingService], }) From a1d05ee7010449ae58171197ae6cf532ab0c20c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 09:37:44 +0000 Subject: [PATCH 19/68] feat(web/helpdesk): public drafting-sheet ticket form + Turnstile widget slot, TICKET LOGGED stamp, IT helpdesk queue card, nav + login links https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/App.tsx | 2 + apps/web/src/Shell.tsx | 1 + apps/web/src/pages/Dept.tsx | 18 +++- apps/web/src/pages/Helpdesk.tsx | 160 ++++++++++++++++++++++++++++++++ apps/web/src/pages/Login.tsx | 5 +- 5 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/pages/Helpdesk.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 03169ff..7bfe13b 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -6,6 +6,7 @@ import Ask from './pages/Ask'; import Dept from './pages/Dept'; import Docs from './pages/Docs'; import Engineering from './pages/Engineering'; +import Helpdesk from './pages/Helpdesk'; import Insights from './pages/Insights'; import Login from './pages/Login'; import ManCom from './pages/ManCom'; @@ -20,6 +21,7 @@ export default function App() { return ( } /> + } /> }> } /> } /> diff --git a/apps/web/src/Shell.tsx b/apps/web/src/Shell.tsx index 06c2bf1..83cd290 100644 --- a/apps/web/src/Shell.tsx +++ b/apps/web/src/Shell.tsx @@ -60,6 +60,7 @@ export default function Shell() {
Work
{item('/approvals', 'Approvals')} {item('/docs', 'Documents')} + {item('/helpdesk', 'Helpdesk')}
Departments
{departments.map(([slug, label]) => item(`/dept/${slug}`, label, true))} diff --git a/apps/web/src/pages/Dept.tsx b/apps/web/src/pages/Dept.tsx index 4999b84..6313f30 100644 --- a/apps/web/src/pages/Dept.tsx +++ b/apps/web/src/pages/Dept.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { api } from '../api'; -import { SheetBar } from '../ui'; +import { Loader, SheetBar } from '../ui'; /** Breadth-first department pages: render the module's read surface as-is. * Deep workflows land per module branch (see CLAUDE.md §11). */ @@ -46,12 +46,17 @@ export default function Dept() { const { slug = '' } = useParams(); const meta = endpoints[slug]; const [data, setData] = useState(null); + const [tickets, setTickets] = useState> | null>(null); const [error, setError] = useState(null); useEffect(() => { setData(null); + setTickets(null); setError(null); if (meta) api(meta.path).then(setData).catch((e: Error) => setError(e.message)); + if (slug === 'it') { + api>>('/tickets?source=it').then(setTickets).catch(() => {}); + } }, [slug]); if (!meta) return
Unknown department: {slug}
; @@ -60,7 +65,7 @@ export default function Dept() { <> {error &&
{error}
} - {!error && data == null &&
Loading…
} + {!error && data == null && } {Array.isArray(data) &&
>} />
} {!Array.isArray(data) && data != null && typeof data === 'object' && (
@@ -76,6 +81,15 @@ export default function Dept() {
)} + {slug === 'it' && tickets && ( +
+
+ Helpdesk queue + SRC · ticketing (it) · public form feeds this +
+ +
+ )} ); } 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/Login.tsx b/apps/web/src/pages/Login.tsx index 38d1956..205904c 100644 --- a/apps/web/src/pages/Login.tsx +++ b/apps/web/src/pages/Login.tsx @@ -1,5 +1,5 @@ import { FormEvent, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import type { UserClaims } from '@ubi/types'; import { api, setSession } from '../api'; @@ -69,6 +69,9 @@ export default function Login() { Offline-ready: after the first online sign-in this device keeps a secure session for field use. Demo: ceo@ubi.ph/ceo123 · vpo@ubi.ph/vpo123 ·{' '} pm@ubi.ph/pm123 · pe@ubi.ph/pe123 +
+ IT problem, no account? File a helpdesk ticket — no sign-in needed → +
From b6a30dc2e6f616e1098840bfd51552b5dbcb589b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 09:37:44 +0000 Subject: [PATCH 20/68] feat(web/ux): typewriter AI responses with thinking state + draft-hatch loaders across pages; proper inline input style on doc transmit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/pages/Approvals.tsx | 4 +-- apps/web/src/pages/Ask.tsx | 35 +++++++++++++++++------- apps/web/src/pages/Docs.tsx | 2 +- apps/web/src/pages/Insights.tsx | 4 +-- apps/web/src/pages/ManCom.tsx | 4 +-- apps/web/src/styles.css | 30 +++++++++++++++++++++ apps/web/src/ui.tsx | 46 ++++++++++++++++++++++++++++++++ 7 files changed, 109 insertions(+), 16 deletions(-) diff --git a/apps/web/src/pages/Approvals.tsx b/apps/web/src/pages/Approvals.tsx index e56a645..51c6a47 100644 --- a/apps/web/src/pages/Approvals.tsx +++ b/apps/web/src/pages/Approvals.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { api } from '../api'; -import { SheetBar, Stamp } from '../ui'; +import { Loader, SheetBar, Stamp } from '../ui'; interface ApprovalRow { id: string; @@ -26,7 +26,7 @@ export default function Approvals() { } if (error) return
{error}
; - if (!rows) return
Loading approvals…
; + if (!rows) return ; return ( <> diff --git a/apps/web/src/pages/Ask.tsx b/apps/web/src/pages/Ask.tsx index 73b49a2..7e8c554 100644 --- a/apps/web/src/pages/Ask.tsx +++ b/apps/web/src/pages/Ask.tsx @@ -1,12 +1,14 @@ -import { FormEvent, useState } from 'react'; +import { FormEvent, useEffect, useRef, useState } from 'react'; import type { AskAnswer } from '@ubi/types'; import { api, getUser } from '../api'; -import { SheetBar } from '../ui'; +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() { @@ -14,17 +16,26 @@ export default function Ask() { 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 }]); + 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 }]); + 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'}` }]); + setTurns((t) => [...t, { who: 'ai', text: `error: ${err instanceof Error ? err.message : 'failed'}`, typed: false }]); } finally { setBusy(false); } @@ -46,19 +57,24 @@ export default function Ask() { )} {turns.map((t, i) => (
- {t.text} - {t.sources && ( + {t.who === 'ai' && !t.typed ? markTyped(i)} /> : t.text} + {t.typed && t.sources && (
{t.sources.map((s) => {s})}
)}
))} - {busy &&
thinking…
} + {busy && ( +
+ Consulting read-models... +
+ )} +
{["What's happening?", 'Why is Package 5 slipping?', 'Which billings are at risk this month?'].map((s) => ( - + ))}
@@ -66,6 +82,7 @@ export default function Ask() { value={q} onChange={(e) => setQ(e.target.value)} placeholder="Ask about any project or department…" + disabled={busy} />
diff --git a/apps/web/src/pages/Docs.tsx b/apps/web/src/pages/Docs.tsx index 2c8a5b0..d4a57b4 100644 --- a/apps/web/src/pages/Docs.tsx +++ b/apps/web/src/pages/Docs.tsx @@ -75,7 +75,7 @@ export default function Docs() {
- setTo(e.target.value)} placeholder="transmit to… (e.g. Finance — Cashier)" /> + setTo(e.target.value)} placeholder="transmit to… (e.g. Finance — Cashier)" />
diff --git a/apps/web/src/pages/Insights.tsx b/apps/web/src/pages/Insights.tsx index 5ca4360..fa3ed91 100644 --- a/apps/web/src/pages/Insights.tsx +++ b/apps/web/src/pages/Insights.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import type { Insight } from '@ubi/types'; import { api } from '../api'; -import { SheetBar, Stamp } from '../ui'; +import { Loader, SheetBar, Stamp } from '../ui'; export default function Insights() { const [items, setItems] = useState(null); @@ -13,7 +13,7 @@ export default function Insights() { }, []); if (error) return
{error}
; - if (!items) return
Loading insights…
; + if (!items) return ; const shown = items.filter((i) => filter === 'all' || i.severity === filter); const counts = (sev: string) => items.filter((i) => i.severity === sev).length; diff --git a/apps/web/src/pages/ManCom.tsx b/apps/web/src/pages/ManCom.tsx index d28f2f0..623b8df 100644 --- a/apps/web/src/pages/ManCom.tsx +++ b/apps/web/src/pages/ManCom.tsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import type { ManComSnapshot } from '@ubi/types'; import { api } from '../api'; import { BarsChart, Sparkline, TrendChart } from '../charts'; -import { Chainage, SheetBar } from '../ui'; +import { Chainage, Loader, SheetBar } from '../ui'; const peso = (centavos: number) => `₱${(centavos / 100 / 1_000_000).toFixed(1)}M`; @@ -24,7 +24,7 @@ export default function ManCom() { } if (error) return
{error}
; - if (!snap) return
Loading ManCom…
; + if (!snap) return ; const k = snap.kpis; return ( diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index fb6ce17..be94219 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -313,3 +313,33 @@ code { font-family: var(--mono); font-size: 0.9em; letter-spacing: -0.2px; } .spread { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } .spread .right { margin-left: auto; } .mb { margin-bottom: 14px; } + +/* ── 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; } diff --git a/apps/web/src/ui.tsx b/apps/web/src/ui.tsx index 5e07f73..ecc10a7 100644 --- a/apps/web/src/ui.tsx +++ b/apps/web/src/ui.tsx @@ -1,5 +1,51 @@ +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}; From a941264c843aa86fd48d8f8ae123e17ad9e9cce8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 22:48:42 +0000 Subject: [PATCH 21/68] feat(dept): standard department dashboard API + dept-head accounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/dept/:slug/dashboard — uniform shape for all 13 departments: 4 standard KPIs, dept-specific field monitoring (Pillar-3 read-model), CCTV camera meta (live snapshot when CCTV_BASE_URL set) - departments registry (head, read-model, camera id per dept) - seed head-@ubi.ph accounts (role:manager + dept:) - RBAC: dept head sees only their dept; CEO/VPO/admin cross-cutting https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/src/app.module.ts | 2 + apps/api/src/modules/dept/dept.module.ts | 157 +++++++++++++++++++++++ apps/api/src/seed/seed.module.ts | 14 +- 3 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/modules/dept/dept.module.ts diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index d0917ec..f5330b8 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -5,6 +5,7 @@ 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 { OperationsModule } from './modules/operations/operations.module'; import { ProcurementModule } from './modules/procurement/procurement.module'; @@ -52,6 +53,7 @@ import { TicketingModule } from './shared/ticketing/ticketing.module'; EngineeringModule, ProcurementModule, OperationsModule, + DeptModule, StubsModule, CeoModule, SeedModule, 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..5de96b6 --- /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.expiry', monitoringTitle: 'Expiring documents & registry', 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/seed/seed.module.ts b/apps/api/src/seed/seed.module.ts index 84a128e..c4ec365 100644 --- a/apps/api/src/seed/seed.module.ts +++ b/apps/api/src/seed/seed.module.ts @@ -7,6 +7,7 @@ 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 { EngineeringModule } from '../modules/engineering/engineering.module'; import { PayItemEntity, ProjectEntity } from '../modules/engineering/entities'; import { ExpiryService } from '../shared/expiry/expiry.module'; @@ -45,6 +46,17 @@ export class SeedService implements OnApplicationBootstrap { }); await this.auth.ensureUser({ email: 'insights-agent@ubi.ph', name: 'Insights Agent (off-box)', password: 'agent123', claims: ['agent:insights'] }); + // Department-head accounts — one per department, each scoped to its dept. + // They land on their own standard department dashboard. + for (const d of DEPARTMENTS) { + await this.auth.ensureUser({ + email: d.headEmail, + name: d.headName, + password: 'head123', + claims: ['role:manager', `dept:${d.slug}`], + }); + } + await this.payItems.save( [ { itemNo: '100(3)', description: 'Clearing and grubbing', unit: 'ha' }, @@ -123,7 +135,7 @@ export class SeedService implements OnApplicationBootstrap { await this.expiry.add({ kind: 'vehicle-reg', refModule: 'records', title: 'Dump truck UBI-DT-114 registration', expiresOn: '2026-06-24' }); await this.expiry.add({ kind: 'insurance', refModule: 'records', title: 'Service vehicle UBI-SV-09 insurance', expiresOn: '2026-06-22' }); - this.log.log('seed complete — login ceo@ubi.ph / ceo123 (also admin, vpo, pm, pe, insights-agent)'); + this.log.log('seed complete — ceo@ubi.ph/ceo123, admin/vpo/pm/pe, + dept heads head-@ubi.ph/head123'); } } From 9146d911168326aec11a16370abf6dc3977fa825 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 22:48:42 +0000 Subject: [PATCH 22/68] =?UTF-8?q?feat(web/dept):=20standard=20department?= =?UTF-8?q?=20dashboard=20=E2=80=94=20KPI=20band,=20field=20monitoring,=20?= =?UTF-8?q?CCTV=20thumbnail;=20dept=20heads=20land=20on=20their=20dashboar?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DeptDashboard renders uniform KPI band + monitoring table + CctvThumb - CctvThumb: NVR snapshot when online, else NO-SIGNAL drafting placeholder - homeFor() routes dept heads to /dept/; Engineering tools at /engineering https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/App.tsx | 7 +-- apps/web/src/api.ts | 8 +++ apps/web/src/pages/Dept.tsx | 112 ++++++++++++++++++++++------------- apps/web/src/pages/Login.tsx | 5 +- apps/web/src/styles.css | 30 ++++++++++ apps/web/src/ui.tsx | 55 +++++++++++++++++ 6 files changed, 168 insertions(+), 49 deletions(-) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 7bfe13b..927630d 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,5 +1,5 @@ import { Navigate, Route, Routes } from 'react-router-dom'; -import { getToken, getUser, hasClaim } from './api'; +import { getToken, getUser, homeFor } from './api'; import Shell from './Shell'; import Approvals from './pages/Approvals'; import Ask from './pages/Ask'; @@ -16,8 +16,7 @@ function RequireAuth({ children }: { children: JSX.Element }) { } export default function App() { - const user = getUser(); - const home = hasClaim(user, 'role:ceo') || hasClaim(user, 'role:vpo') ? '/mancom' : '/approvals'; + const home = homeFor(getUser()?.claims ?? []); return ( } /> @@ -29,7 +28,7 @@ export default function App() { } /> } /> } /> - } /> + } /> } /> } /> diff --git a/apps/web/src/api.ts b/apps/web/src/api.ts index 087fafc..8529c5b 100644 --- a/apps/web/src/api.ts +++ b/apps/web/src/api.ts @@ -48,3 +48,11 @@ export async function api(path: string, init?: RequestInit): Promise { 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/pages/Dept.tsx b/apps/web/src/pages/Dept.tsx index 6313f30..62570f0 100644 --- a/apps/web/src/pages/Dept.tsx +++ b/apps/web/src/pages/Dept.tsx @@ -1,24 +1,16 @@ import { useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import { Link, useParams } from 'react-router-dom'; import { api } from '../api'; -import { Loader, SheetBar } from '../ui'; +import { CctvThumb, KpiBand, Loader, SheetBar } from '../ui'; -/** Breadth-first department pages: render the module's read surface as-is. - * Deep workflows land per module branch (see CLAUDE.md §11). */ -const endpoints: Record = { - procurement: { title: 'Procurement — supplier performance', path: '/procurement/supplier-performance', src: 'procurement.supplier-performance' }, - operations: { title: 'Operations — VPO control tower', path: '/operations/health', src: 'operations.health' }, - survey: { title: 'Survey — monthly volumes', path: '/survey/volumes', src: 'survey.volumes' }, - mqc: { title: 'MQC — certificate queue', path: '/mqc/certs', src: 'mqc.certs' }, - audit: { title: 'Audit — exception register', path: '/audit/exceptions', src: 'audit.exceptions' }, - it: { title: 'IT — capacity planning & controls', path: '/it/capacity', src: 'it.capacity' }, - records: { title: 'Records — expiring documents', path: '/records/expiry', src: 'records.expiry' }, - clinic: { title: 'Clinic — aggregate summary (isolated)', path: '/clinic/aggregate', src: 'clinic.aggregate' }, - admin: { title: 'Admin / OHS — inquiries (shared ticketing)', path: '/tickets?source=admin', src: 'admin.tickets' }, - hr: { title: 'HR — hours computation runs', path: '/hr/hours', src: 'hr.hours' }, - property: { title: 'Property — asset custody (QR EAM)', path: '/property/assets', src: 'property.assets' }, - finance: { title: 'Finance — documents in motion', path: '/finance/doc-flow', src: 'finance.doc-flow' }, -}; +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 }; +} function Value({ v }: { v: unknown }) { if (v == null) return ; @@ -42,45 +34,81 @@ function AutoTable({ rows }: { rows: Array> }) { ); } +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 meta = endpoints[slug]; - const [data, setData] = useState(null); + const [dash, setDash] = useState(null); const [tickets, setTickets] = useState> | null>(null); const [error, setError] = useState(null); useEffect(() => { - setData(null); + setDash(null); setTickets(null); setError(null); - if (meta) api(meta.path).then(setData).catch((e: Error) => setError(e.message)); - if (slug === 'it') { - api>>('/tickets?source=it').then(setTickets).catch(() => {}); - } + api(`/dept/${slug}/dashboard`).then(setDash).catch((e: Error) => setError(e.message)); + if (slug === 'it') api>>('/tickets?source=it').then(setTickets).catch(() => {}); }, [slug]); - if (!meta) return
Unknown department: {slug}
; + if (error) return <>
{error}
; + if (!dash) return <>; return ( <> - - {error &&
{error}
} - {!error && data == null && } - {Array.isArray(data) &&
>} />
} - {!Array.isArray(data) && data != null && typeof data === 'object' && ( -
- - - {Object.entries(data as Record).map(([k, v]) => ( - - - - - ))} - -
{k}{Array.isArray(v) ? >} /> : }
+ + + {slug === 'engineering' && ( +
+ Open Engineering tools — projects · materials schedule →
)} + + + +
+
+
+ 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 && (
diff --git a/apps/web/src/pages/Login.tsx b/apps/web/src/pages/Login.tsx index 205904c..4459c30 100644 --- a/apps/web/src/pages/Login.tsx +++ b/apps/web/src/pages/Login.tsx @@ -1,7 +1,7 @@ import { FormEvent, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import type { UserClaims } from '@ubi/types'; -import { api, setSession } from '../api'; +import { api, homeFor, setSession } from '../api'; export default function Login() { const [email, setEmail] = useState(''); @@ -20,8 +20,7 @@ export default function Login() { body: JSON.stringify({ email, password }), }); setSession(res.token, res.user); - const executive = res.user.claims.some((c) => ['role:ceo', 'role:vpo', 'role:admin'].includes(c)); - navigate(executive ? '/mancom' : '/approvals'); + navigate(homeFor(res.user.claims)); } catch (err) { setError(err instanceof Error ? err.message : 'login failed'); } finally { diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index be94219..ad6c13b 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -309,6 +309,36 @@ code { font-family: var(--mono); font-size: 0.9em; letter-spacing: -0.2px; } .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; } +/* ── 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; } diff --git a/apps/web/src/ui.tsx b/apps/web/src/ui.tsx index ecc10a7..8757c13 100644 --- a/apps/web/src/ui.tsx +++ b/apps/web/src/ui.tsx @@ -77,6 +77,61 @@ export function Chainage({ pct, label, tone = 'teal' }: { pct: number; label?: s ); } +/** 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 ( From 35209bcf7292004c1077d9a73eb6400041c4b656 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 22:48:42 +0000 Subject: [PATCH 23/68] docs(audit): feature audit vs spec (IT/Property/Records) + dept-dashboard status https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- AUDIT.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 AUDIT.md diff --git a/AUDIT.md b/AUDIT.md new file mode 100644 index 0000000..37db2f2 --- /dev/null +++ b/AUDIT.md @@ -0,0 +1,61 @@ +# Feature Audit vs Original Spec — 2026-06-14 + +Measured against `CLAUDE.md` Part II. Status of the modules you asked about +first (IT, Property, Records/Doc-Tracking), plus the cross-cutting +department-dashboard layer added this session. + +> **Live-site note:** `edge.ubi-as.com` was running an **older build** at audit +> time — `POST /api/public/helpdesk` returned 404, so the public helpdesk + +> Turnstile work was not yet deployed. Pull `kickoff-build` and rebuild on the +> box to get everything below. + +## Legend +✅ built · 🟡 stub / read-surface only · ❌ not started + +## IT module +| Spec feature | Status | Notes | +|---|---|---| +| AI Helpdesk (landing, queue) | ✅ | Public no-signin form + Turnstile gate; feeds shared ticketing; IT dept dashboard shows the queue | +| Capacity planning & controls | 🟡 | `it.capacity` read-model with storage/transfer/DB/UPS rows + amber/red thresholds (stub data) | +| SNMP device monitoring + map | ❌ | not started | +| DeskGuard v2 (client OCR + KPIs) | ❌ | not started | +| Maintenance scheduler (PMS) | ❌ | not started | +| Environmental monitoring (DHT22/ESP32) | ❌ | not started | + +## Property module +| Spec feature | Status | Notes | +|---|---|---| +| Asset custody read surface | 🟡 | `property.assets` stub rows (QR, type, custodian, location, status) | +| QR scan → location/status/custodian update | ❌ | no scan flow, no `asset_movement` table | +| Monthly custodian attestation | ❌ | not started | +| Feed IT PMS scheduler | ❌ | depends on PMS | + +## Records / Document Tracking +| Spec feature | Status | Notes | +|---|---|---| +| Document tracking (Pillar 2) | ✅ | QR codes, scan transmit/receive, where-is + full history — real | +| Expiry/renewal reminders | ✅ | shared `ExpiryModule`; `records.expiry` read-model | +| Vehicle reg/insurance/stamp registry | ❌ | no `vehicle_doc` entity yet | +| Physical-location index (access-gated) | ❌ | not started | + +## Cross-cutting: Department dashboards (NEW this session) +| Capability | Status | Notes | +|---|---|---| +| Department-head accounts | ✅ | `head-@ubi.ph` / `head123`, claims `role:manager` + `dept:` (demo seed) | +| Standard KPI dashboard (uniform all depts) | ✅ | 4 cards: Monitored items · Reporting · Open approvals · Needs attention — same shape everywhere | +| Per-dept field monitoring | ✅ | renders that dept's Pillar-3 read-model | +| CCTV thumbnail per dept | ✅ | tile per `CAM--01`; live snapshot when `CCTV_BASE_URL` set, else honest NO-SIGNAL placeholder | +| RBAC isolation | ✅ | dept head sees only their dept (403 otherwise); CEO/VPO/admin cross-cutting | + +Endpoint: `GET /api/dept/:slug/dashboard` · registry in +`apps/api/src/modules/dept/dept.module.ts`. + +## Suggested next build order +1. **Property QR EAM** — `asset` + `asset_movement` + scan endpoints + monthly + attestation (branch `property`). +2. **Records** — `vehicle_doc` registry + access-gated physical-location index + (branch `records`). +3. **IT depth** — PMS scheduler (pulls Property assets), then SNMP map, then + DeskGuard v2 (branch `it`). +4. **CCTV** — set `CCTV_BASE_URL` to the Hikvision NVR snapshot base so every + dept tile goes live. From c7328c4d92ee5bc51000926b1602199683b7e434 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 23:09:03 +0000 Subject: [PATCH 24/68] =?UTF-8?q?feat(property):=20real=20QR=20EAM=20?= =?UTF-8?q?=E2=80=94=20asset=20+=20movement=20+=20attestation,=20scan-to-u?= =?UTF-8?q?pdate,=20history;=20feeds=20IT=20PMS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- .../src/modules/property/property.module.ts | 258 ++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 apps/api/src/modules/property/property.module.ts 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..23dfcd4 --- /dev/null +++ b/apps/api/src/modules/property/property.module.ts @@ -0,0 +1,258 @@ +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; + + @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 } }); + } + + async seed(creator: string): Promise { + if ((await this.assets.count()) > 0) return; + const rows: Array> = [ + { qr: 'AST-00112', type: 'Total station (Leica TS07)', status: 'in-use', location: 'PKG-02 site office', custodian: 'Survey — Reyes' }, + { qr: 'AST-00387', type: 'Plate compactor', status: 'in-use', location: 'PKG-05', custodian: 'PKG-05 warehouse' }, + { qr: 'AST-00421', type: 'Laptop (Dell 5440)', status: 'in-use', location: 'HQ 3F Engineering', custodian: 'Engr. dela Cruz' }, + { qr: 'AST-00455', type: 'Concrete vibrator', status: 'maintenance', location: 'HQ motor pool', custodian: 'Property warehouse' }, + { qr: 'AST-00501', type: 'Generator 10kVA', status: 'idle', location: 'PKG-11 yard', custodian: 'PKG-11 warehouse' }, + ]; + 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 {} From 3b31ed935222390bd4f4a7f72ae63722c126124e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 23:09:03 +0000 Subject: [PATCH 25/68] feat(records): vehicle-doc registry (mirrors to expiry engine) + access-gated physical-location index https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- .../api/src/modules/records/records.module.ts | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 apps/api/src/modules/records/records.module.ts 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..686bac8 --- /dev/null +++ b/apps/api/src/modules/records/records.module.ts @@ -0,0 +1,210 @@ +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 { 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 registry: ReadModelRegistry, + ) {} + + onModuleInit(): void { + 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), + ); + } + + 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('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])], + controllers: [RecordsController], + providers: [RecordsService], + exports: [RecordsService], +}) +export class RecordsModule {} From d6900a0e71f6dca652a4c4b0edf20ced14b7f34a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 23:09:04 +0000 Subject: [PATCH 26/68] feat(it): SNMP device inventory, quarterly PMS auto-built from Property assets, DeskGuard v2 + consent, env monitoring https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/src/modules/it/it.module.ts | 200 +++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 apps/api/src/modules/it/it.module.ts 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..3c6e826 --- /dev/null +++ b/apps/api/src/modules/it/it.module.ts @@ -0,0 +1,200 @@ +import { Body, Controller, Get, Injectable, Module, OnModuleInit, Post } from '@nestjs/common'; +import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import type { UserClaims } from '@ubi/types'; +import { 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; +} + +/** 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() quarter: string; // e.g. 2026-Q2 + @Column({ name: 'due_on', type: 'date' }) dueOn: string; + @Column({ type: 'varchar', default: 'scheduled' }) status: 'scheduled' | 'done'; +} + +/** 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; +} + +@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' } }); } + listPms(): Promise { return this.pms.find({ order: { dueOn: 'ASC' }, take: 200 }); } + listEnv(): Promise { return this.env.find({ order: { createdAt: 'DESC' }, take: 50 }); } + listDeskguard(): Promise { return this.desk.find({ order: { createdAt: 'DESC' }, take: 100 }); } + + addDevice(user: UserClaims, dto: DeviceDto): Promise { + return this.devices.save(this.devices.create({ ...dto, status: 'up', createdBy: user.email })); + } + + /** Auto-generate quarterly PMS from active Property assets (idempotent/quarter). */ + async generatePms(user: UserClaims): Promise<{ created: number; quarter: string }> { + const now = new Date(); + const q = `${now.getFullYear()}-Q${Math.floor(now.getMonth() / 3) + 1}`; + const assets = await this.property.pmsEligible(); + let created = 0; + for (const a of assets) { + const exists = await this.pms.findOne({ where: { assetQr: a.qr, quarter: q } }); + if (exists) continue; + const due = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3 + 2, 28); + await this.pms.save( + this.pms.create({ + assetQr: a.qr, assetType: a.type, quarter: q, + dueOn: due.toISOString().slice(0, 10), status: 'scheduled', 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' }), + ]); + } + await this.generatePms(user); + } +} + +@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('pms') pmsList(): Promise { return this.svc.listPms(); } + @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(); } + + @Post('devices') addDevice(@CurrentUser() u: UserClaims, @Body() dto: DeviceDto): Promise { return this.svc.addDevice(u, dto); } + @Post('pms/generate') gen(@CurrentUser() u: UserClaims): Promise<{ created: number; quarter: string }> { return this.svc.generatePms(u); } + @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 {} From 90ab7e642afceeb90882c743c10b30925a514201 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 23:09:04 +0000 Subject: [PATCH 27/68] chore(api): wire property/records/it modules, remove superseded stubs, seed real module data https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/src/app.module.ts | 6 +++ apps/api/src/modules/stubs/stubs.module.ts | 43 ++-------------------- apps/api/src/seed/seed.module.ts | 14 +++++-- 3 files changed, 20 insertions(+), 43 deletions(-) diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index f5330b8..d5fb80f 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -7,8 +7,11 @@ 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 { ItModule } from './modules/it/it.module'; import { OperationsModule } from './modules/operations/operations.module'; import { ProcurementModule } from './modules/procurement/procurement.module'; +import { PropertyModule } from './modules/property/property.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'; @@ -53,6 +56,9 @@ import { TicketingModule } from './shared/ticketing/ticketing.module'; EngineeringModule, ProcurementModule, OperationsModule, + PropertyModule, + RecordsModule, + ItModule, DeptModule, StubsModule, CeoModule, diff --git a/apps/api/src/modules/stubs/stubs.module.ts b/apps/api/src/modules/stubs/stubs.module.ts index bbfc325..657c29f 100644 --- a/apps/api/src/modules/stubs/stubs.module.ts +++ b/apps/api/src/modules/stubs/stubs.module.ts @@ -68,30 +68,8 @@ export class StubReadModelsService implements OnModuleInit { ], ); - this.registry.register( - { - key: 'it.capacity', - module: 'it', - title: 'Capacity planning & controls', - description: 'Resource headroom + projected breach dates (scaling triggers).', - }, - () => [ - { 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' }, - ], - ); - - this.registry.register( - { - key: 'records.expiry', - module: 'records', - title: 'Expiring registrations & insurance', - description: 'Vehicle reg, insurance, stamps — alarm before lapse.', - }, - () => this.expiry.upcoming(30), - ); + // NOTE: it.capacity, records.expiry and property.assets are now owned by + // the real ItModule / RecordsModule / PropertyModule. this.registry.register( { @@ -129,19 +107,6 @@ export class StubReadModelsService implements OnModuleInit { () => [{ run: '2026-06-A', period: 'Jun 1–15', status: 'computed', pushedToEzacctg: false, employees: 487 }], ); - this.registry.register( - { - key: 'property.assets', - module: 'property', - title: 'Asset custody (QR EAM)', - description: 'Who holds what; monthly custodian attestation trail.', - }, - () => [ - { qr: 'AST-00112', type: 'Total station', custodian: 'Survey — Reyes', location: 'PKG-02 site office', status: 'in-use' }, - { qr: 'AST-00387', type: 'Plate compactor', custodian: 'PKG-05 warehouse', location: 'PKG-05', status: 'in-use' }, - ], - ); - this.registry.register( { key: 'finance.doc-flow', @@ -173,14 +138,12 @@ export class StubsController { : { error: `read model ${key} not registered` }; } + // Real modules (it, records, property) own their own routes now. @Get('survey/volumes') survey(): unknown { return this.query('survey.volumes'); } @Get('mqc/certs') mqc(): unknown { return this.query('mqc.certs'); } @Get('audit/exceptions') audit(): unknown { return this.query('audit.exceptions'); } - @Get('it/capacity') it(): unknown { return this.query('it.capacity'); } - @Get('records/expiry') records(): unknown { return this.query('records.expiry'); } @Get('clinic/aggregate') clinic(): unknown { return this.query('clinic.aggregate'); } @Get('hr/hours') hr(): unknown { return this.query('hr.hours'); } - @Get('property/assets') property(): unknown { return this.query('property.assets'); } @Get('finance/doc-flow') finance(): unknown { return this.query('finance.doc-flow'); } } diff --git a/apps/api/src/seed/seed.module.ts b/apps/api/src/seed/seed.module.ts index c4ec365..c0273cf 100644 --- a/apps/api/src/seed/seed.module.ts +++ b/apps/api/src/seed/seed.module.ts @@ -10,6 +10,9 @@ import { DocTrackingService } from '../pillars/doc-tracking/doc-tracking.service import { DEPARTMENTS } from '../modules/dept/dept.module'; import { EngineeringModule } from '../modules/engineering/engineering.module'; import { PayItemEntity, ProjectEntity } from '../modules/engineering/entities'; +import { ItModule, ItService } from '../modules/it/it.module'; +import { PropertyModule, PropertyService } from '../modules/property/property.module'; +import { RecordsModule, RecordsService } from '../modules/records/records.module'; import { ExpiryService } from '../shared/expiry/expiry.module'; /** Dev/demo seed — idempotent; runs only when the database is empty. */ @@ -22,6 +25,9 @@ export class SeedService implements OnApplicationBootstrap { private readonly auth: AuthService, private readonly docs: DocTrackingService, private readonly expiry: ExpiryService, + private readonly property: PropertyService, + private readonly records: RecordsService, + private readonly it: ItService, @InjectRepository(PayItemEntity) private readonly payItems: Repository, @InjectRepository(ProjectEntity) private readonly projects: Repository, @InjectRepository(InsightEntity) private readonly insights: Repository, @@ -132,15 +138,17 @@ export class SeedService implements OnApplicationBootstrap { }); await this.docs.transmit(ceoClaims, doc.code, 'Finance — J. Ramos', 'for cashier batching'); - await this.expiry.add({ kind: 'vehicle-reg', refModule: 'records', title: 'Dump truck UBI-DT-114 registration', expiresOn: '2026-06-24' }); - await this.expiry.add({ kind: 'insurance', refModule: 'records', title: 'Service vehicle UBI-SV-09 insurance', expiresOn: '2026-06-22' }); + // Real department modules — assets, vehicle docs, physical index, IT. + await this.property.seed('seed'); + await this.records.seed('seed'); + await this.it.seed(ceoClaims); this.log.log('seed complete — ceo@ubi.ph/ceo123, admin/vpo/pm/pe, + dept heads head-@ubi.ph/head123'); } } @Module({ - imports: [DocTrackingModule, EngineeringModule], + imports: [DocTrackingModule, EngineeringModule, PropertyModule, RecordsModule, ItModule], providers: [SeedService], }) export class SeedModule {} From 5b16fee3d4d14c9fbc7abc12a9ef564e42475539 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 23:09:04 +0000 Subject: [PATCH 28/68] feat(web/auth): clickable demo-login chips (autofill) + department-head selector https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/pages/Login.tsx | 42 +++++++++++++++++++++++++++++++++--- apps/web/src/styles.css | 6 ++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/apps/web/src/pages/Login.tsx b/apps/web/src/pages/Login.tsx index 4459c30..912a46c 100644 --- a/apps/web/src/pages/Login.tsx +++ b/apps/web/src/pages/Login.tsx @@ -3,6 +3,19 @@ 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', +]; + export default function Login() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -10,6 +23,12 @@ export default function Login() { 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); @@ -64,10 +83,27 @@ export default function Login() {
RevA
+
+
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. Demo: ceo@ubi.ph/ceo123 · vpo@ubi.ph/vpo123 ·{' '} - pm@ubi.ph/pm123 · pe@ubi.ph/pe123 + 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/styles.css b/apps/web/src/styles.css index ad6c13b..3c0128d 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -299,6 +299,12 @@ code { font-family: var(--mono); font-size: 0.9em; letter-spacing: -0.2px; } .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; } From 4a8f4e8fd06dc274e7ccf5414901ba5ba8144ee6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 23:09:04 +0000 Subject: [PATCH 29/68] feat(web): Property/Records/IT tool pages + dept-dashboard tool links - Property: scan/re-custody/attest + movement history - Records: expiry, add vehicle doc, access-gated physical index - IT control room: devices, PMS, DeskGuard (consent banner), environmental https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/App.tsx | 6 ++ apps/web/src/pages/Dept.tsx | 12 +++- apps/web/src/pages/It.tsx | 106 +++++++++++++++++++++++++++++ apps/web/src/pages/Property.tsx | 115 ++++++++++++++++++++++++++++++++ apps/web/src/pages/Records.tsx | 78 ++++++++++++++++++++++ 5 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/pages/It.tsx create mode 100644 apps/web/src/pages/Property.tsx create mode 100644 apps/web/src/pages/Records.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 927630d..7b51d04 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -8,8 +8,11 @@ import Docs from './pages/Docs'; import Engineering from './pages/Engineering'; 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 Property from './pages/Property'; +import Records from './pages/Records'; function RequireAuth({ children }: { children: JSX.Element }) { return getToken() ? children : ; @@ -29,6 +32,9 @@ export default function App() { } /> } /> } /> + } /> + } /> + } /> } /> } /> diff --git a/apps/web/src/pages/Dept.tsx b/apps/web/src/pages/Dept.tsx index 62570f0..aad79f4 100644 --- a/apps/web/src/pages/Dept.tsx +++ b/apps/web/src/pages/Dept.tsx @@ -12,6 +12,14 @@ interface DeptDashboard { 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' }, + 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 · DeskGuard · env' }, +}; + function Value({ v }: { v: unknown }) { if (v == null) return ; if (typeof v === 'boolean') return {String(v)}; @@ -80,9 +88,9 @@ export default function Dept() { note={`Head: ${dash.head.name} · monitoring ${dash.monitoring.src}`} /> - {slug === 'engineering' && ( + {TOOLS[slug] && (
- Open Engineering tools — projects · materials schedule → + {TOOLS[slug].label} →
)} diff --git a/apps/web/src/pages/It.tsx b/apps/web/src/pages/It.tsx new file mode 100644 index 0000000..2ee5f76 --- /dev/null +++ b/apps/web/src/pages/It.tsx @@ -0,0 +1,106 @@ +import { useEffect, useState } from 'react'; +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 · auto-built from Property assets
+ + + + {pms.map((p) => ( + + + + + ))} + +
AssetTypeQuarterDueStatus
{p.assetQr}{p.assetType}{p.quarter}{p.dueOn}{p.status}
+
+ +
+
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/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/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).
+
+
+ + ); +} From 8217c3fa53ad540dc90f75109348f46cdc9fd814 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 23:09:04 +0000 Subject: [PATCH 30/68] docs(audit): record build-all results (property/records/it now real, 21 read-models) https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- AUDIT.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/AUDIT.md b/AUDIT.md index 37db2f2..592f536 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -1,5 +1,9 @@ # Feature Audit vs Original Spec — 2026-06-14 +> **Update (build-all pass):** Property, Records and IT promoted from stubs to +> real DB-backed modules — see "Build-all results" at the bottom. + + Measured against `CLAUDE.md` Part II. Status of the modules you asked about first (IT, Property, Records/Doc-Tracking), plus the cross-cutting department-dashboard layer added this session. @@ -59,3 +63,39 @@ Endpoint: `GET /api/dept/:slug/dashboard` · registry in DeskGuard v2 (branch `it`). 4. **CCTV** — set `CCTV_BASE_URL` to the Hikvision NVR snapshot base so every dept tile goes live. + +--- + +## Build-all results — 2026-06-14 (this session) + +### Property — ✅ real QR EAM +`asset` + `asset_movement` + `custodian_attestation` tables. Endpoints: +scan (`POST /property/assets/:qr/scan` — updates location/status/custodian + +logs movement), monthly attestation, full movement history. Tools page +`/property`. Active assets feed the IT PMS scheduler. + +### Records — ✅ real +`vehicle_doc` registry (reg/insurance/stamp) auto-mirrors into the shared +expiry engine; `records.expiry` now DB-backed. `physical_location` index is +**access-gated per row** — restricted titles (e.g. land titles → `role:legal`) +are masked unless the caller holds the claim (verified: records head sees +dept rows, legal row stays locked; IT head sees all locked). Tools page +`/records`. + +### IT — ✅ depth added +`it_device` (SNMP targets: UPS/switch/NVR/firewall), `it_pms` (quarterly, +**auto-generated from Property assets**), `deskguard_entry` (self-report + +mandatory consent banner surfaced in UI), `env_reading` (DHT22/IPMI temp). +Read-models: it.capacity, it.devices, it.pms, it.deskguard, it.env. Control +room page `/it`. + +### Cross-cutting +- 21 Pillar-3 read-models registered (was 15) — all flow into dept dashboards, + ManCom and Ask AI. +- Login page: clickable demo-login chips (autofill) + dept-head selector. + +### Still stub / not started +- IT SNMP **live polling** + topology map (device list is real, polling is not). +- Survey, MQC, Audit, Clinic, Admin, HR, Finance remain read-surface stubs + (render in dept dashboards; deep workflows pending per module branch). +- CCTV needs `CCTV_BASE_URL` → Hikvision NVR to go live. From 48c0b135d021788737d67daad2866c10ee8ce8ed Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:07:07 +0000 Subject: [PATCH 31/68] feat(property): asset category + category helpers for PMS selection https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- .../src/modules/property/property.module.ts | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/apps/api/src/modules/property/property.module.ts b/apps/api/src/modules/property/property.module.ts index 23dfcd4..0614283 100644 --- a/apps/api/src/modules/property/property.module.ts +++ b/apps/api/src/modules/property/property.module.ts @@ -29,6 +29,10 @@ export class AssetEntity extends BaseAppEntity { @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; @@ -197,14 +201,30 @@ export class PropertyService implements OnModuleInit { 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)', status: 'in-use', location: 'PKG-02 site office', custodian: 'Survey — Reyes' }, - { qr: 'AST-00387', type: 'Plate compactor', status: 'in-use', location: 'PKG-05', custodian: 'PKG-05 warehouse' }, - { qr: 'AST-00421', type: 'Laptop (Dell 5440)', status: 'in-use', location: 'HQ 3F Engineering', custodian: 'Engr. dela Cruz' }, - { qr: 'AST-00455', type: 'Concrete vibrator', status: 'maintenance', location: 'HQ motor pool', custodian: 'Property warehouse' }, - { qr: 'AST-00501', type: 'Generator 10kVA', status: 'idle', location: 'PKG-11 yard', custodian: 'PKG-11 warehouse' }, + { 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 })); From 1a0c99b6e6087400b28bf21b8aef4244f9ff6f82 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:07:07 +0000 Subject: [PATCH 32/68] feat(it/pms): category-based quarterly generation, tech + completed date, scheduled/in-progress/done flow, per-quarter listing https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/src/modules/it/it.module.ts | 99 +++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 16 deletions(-) diff --git a/apps/api/src/modules/it/it.module.ts b/apps/api/src/modules/it/it.module.ts index 3c6e826..43d83ba 100644 --- a/apps/api/src/modules/it/it.module.ts +++ b/apps/api/src/modules/it/it.module.ts @@ -1,7 +1,7 @@ -import { Body, Controller, Get, Injectable, Module, OnModuleInit, Post } from '@nestjs/common'; +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 { IsInt, IsOptional, IsString, MinLength } from 'class-validator'; +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'; @@ -19,14 +19,19 @@ export class DeviceEntity extends BaseAppEntity { @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() quarter: string; // e.g. 2026-Q2 + @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: 'scheduled' | 'done'; + @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). */ @@ -67,6 +72,18 @@ class DeskguardDto { @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( @@ -112,28 +129,58 @@ export class ItService implements OnModuleInit { } listDevices(): Promise { return this.devices.find({ order: { kind: 'ASC' } }); } - listPms(): Promise { return this.pms.find({ order: { dueOn: 'ASC' }, take: 200 }); } 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 })); } - /** Auto-generate quarterly PMS from active Property assets (idempotent/quarter). */ - async generatePms(user: UserClaims): Promise<{ created: number; quarter: string }> { - const now = new Date(); - const q = `${now.getFullYear()}-Q${Math.floor(now.getMonth() / 3) + 1}`; - const assets = await this.property.pmsEligible(); + /** 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; - const due = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3 + 2, 28); await this.pms.save( this.pms.create({ - assetQr: a.qr, assetType: a.type, quarter: q, - dueOn: due.toISOString().slice(0, 10), status: 'scheduled', createdBy: user?.email ?? 'system', + assetQr: a.qr, assetType: a.type, category: a.category, quarter: q, + dueOn, status: 'scheduled', tech: null, completedOn: null, createdBy: user?.email ?? 'system', }), ); created += 1; @@ -171,7 +218,20 @@ export class ItService implements OnModuleInit { 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' }), ]); } - await this.generatePms(user); + // 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] }); + } } } @@ -181,13 +241,20 @@ export class ItController { @Get('capacity') capacity(): unknown { return this.svc.capacity(); } @Get('devices') devices(): Promise { return this.svc.listDevices(); } - @Get('pms') pmsList(): Promise { return this.svc.listPms(); } @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('pms/generate') gen(@CurrentUser() u: UserClaims): Promise<{ created: number; quarter: string }> { return this.svc.generatePms(u); } @Post('deskguard') post(@CurrentUser() u: UserClaims, @Body() dto: DeskguardDto): Promise { return this.svc.postDeskguard(u, dto); } } From f4953a8148d9e0256642fde85cbf1fab7b2fed07 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:07:07 +0000 Subject: [PATCH 33/68] feat(records): Records owns document-registry monitoring (records.documents); Finance narrowed to its own batches (consumer, not monitor) https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/src/modules/dept/dept.module.ts | 2 +- .../api/src/modules/records/records.module.ts | 20 ++++++++++++++++++- apps/api/src/modules/stubs/stubs.module.ts | 12 +++++------ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/apps/api/src/modules/dept/dept.module.ts b/apps/api/src/modules/dept/dept.module.ts index 5de96b6..899f036 100644 --- a/apps/api/src/modules/dept/dept.module.ts +++ b/apps/api/src/modules/dept/dept.module.ts @@ -26,7 +26,7 @@ export const DEPARTMENTS: DeptDef[] = [ { 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.expiry', monitoringTitle: 'Expiring documents & registry', cameraId: 'CAM-REC-01', headEmail: 'head-records@ubi.ph', headName: 'R. Bautista' }, + { 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' }, diff --git a/apps/api/src/modules/records/records.module.ts b/apps/api/src/modules/records/records.module.ts index 686bac8..f6f90aa 100644 --- a/apps/api/src/modules/records/records.module.ts +++ b/apps/api/src/modules/records/records.module.ts @@ -15,6 +15,8 @@ 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. */ @@ -76,10 +78,17 @@ export class RecordsService implements OnModuleInit { @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(), @@ -96,6 +105,10 @@ export class RecordsService implements OnModuleInit { ); } + documents(): ReturnType { + return this.docs.list(); + } + async upcoming(): Promise> { const now = Date.now(); const fromVehicles = (await this.vdocs.find()).map((v) => ({ @@ -180,6 +193,11 @@ export class RecordsService implements OnModuleInit { export class RecordsController { constructor(private readonly svc: RecordsService) {} + @Get('documents') + documents(): Promise { + return this.svc.documents(); + } + @Get('expiry') expiry(): Promise { return this.svc.upcoming(); @@ -202,7 +220,7 @@ export class RecordsController { } @Module({ - imports: [TypeOrmModule.forFeature([VehicleDocEntity, PhysicalLocationEntity])], + imports: [TypeOrmModule.forFeature([VehicleDocEntity, PhysicalLocationEntity]), DocTrackingModule], controllers: [RecordsController], providers: [RecordsService], exports: [RecordsService], diff --git a/apps/api/src/modules/stubs/stubs.module.ts b/apps/api/src/modules/stubs/stubs.module.ts index 657c29f..ffd3841 100644 --- a/apps/api/src/modules/stubs/stubs.module.ts +++ b/apps/api/src/modules/stubs/stubs.module.ts @@ -111,16 +111,14 @@ export class StubReadModelsService implements OnModuleInit { { key: 'finance.doc-flow', module: 'finance', - title: 'Document batches in motion', - description: 'Where is this paper now — batches to cashier, incoming from Records.', + 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 { - inTransit: docs.filter((d) => d.status === 'in-transit').length, - withFinance: docs.filter((d) => d.currentHolder.toLowerCase().includes('finance')).length, - recent: docs.slice(0, 10), - }; + return docs.filter( + (d) => d.currentHolder.toLowerCase().includes('finance') || d.currentLocation.toLowerCase().includes('cashier'), + ); }, ); } From 4dd520b0c52382c8cdb0b864ab2523e09a8b9049 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:07:07 +0000 Subject: [PATCH 34/68] =?UTF-8?q?feat(web/it):=20PMS=20scheduler=20?= =?UTF-8?q?=E2=80=94=20Q1-Q4=20tabs,=20category-chip=20generation,=20list/?= =?UTF-8?q?kanban/calendar=20views,=20tech=20assign=20+=20status=20advance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/App.tsx | 2 + apps/web/src/pages/Dept.tsx | 2 +- apps/web/src/pages/It.tsx | 18 +-- apps/web/src/pages/PmsScheduler.tsx | 173 ++++++++++++++++++++++++++++ apps/web/src/styles.css | 30 +++++ 5 files changed, 212 insertions(+), 13 deletions(-) create mode 100644 apps/web/src/pages/PmsScheduler.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 7b51d04..c78a4d1 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -11,6 +11,7 @@ import Insights from './pages/Insights'; import It from './pages/It'; import Login from './pages/Login'; import ManCom from './pages/ManCom'; +import PmsScheduler from './pages/PmsScheduler'; import Property from './pages/Property'; import Records from './pages/Records'; @@ -35,6 +36,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> diff --git a/apps/web/src/pages/Dept.tsx b/apps/web/src/pages/Dept.tsx index aad79f4..1b2de99 100644 --- a/apps/web/src/pages/Dept.tsx +++ b/apps/web/src/pages/Dept.tsx @@ -17,7 +17,7 @@ const TOOLS: Record = { engineering: { path: '/engineering', label: 'Open Engineering tools — projects · materials schedule' }, 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 · DeskGuard · env' }, + it: { path: '/it', label: 'Open IT control room — devices · PMS scheduler · DeskGuard · env' }, }; function Value({ v }: { v: unknown }) { diff --git a/apps/web/src/pages/It.tsx b/apps/web/src/pages/It.tsx index 2ee5f76..5b35c56 100644 --- a/apps/web/src/pages/It.tsx +++ b/apps/web/src/pages/It.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; import { api } from '../api'; import { Loader, SheetBar } from '../ui'; @@ -70,18 +71,11 @@ export default function It() {
-
Preventive maintenance (PMS)SRC · it.pms · auto-built from Property assets
- - - - {pms.map((p) => ( - - - - - ))} - -
AssetTypeQuarterDueStatus
{p.assetQr}{p.assetType}{p.quarter}{p.dueOn}{p.status}
+
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 → +
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/styles.css b/apps/web/src/styles.css index 3c0128d..2d7faa0 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -350,6 +350,36 @@ code { font-family: var(--mono); font-size: 0.9em; letter-spacing: -0.2px; } .spread .right { margin-left: auto; } .mb { margin-bottom: 14px; } +/* ── 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 { From 01fb51a1b4f021af3efa3383bc3a624073dc8d2d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:34:06 +0000 Subject: [PATCH 35/68] feat(types): materials catalog + quantity contracts (weekly entry, rollup, projection, material reqs) + ManCom accomplishment fields https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- packages/types/src/index.ts | 1 + packages/types/src/mancom.ts | 14 +++++++ packages/types/src/quantity.ts | 72 ++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 packages/types/src/quantity.ts diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 4206d19..04c942f 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -2,3 +2,4 @@ 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 index 0f35c98..aab7dc9 100644 --- a/packages/types/src/mancom.ts +++ b/packages/types/src/mancom.ts @@ -12,6 +12,10 @@ export interface ManComKpis { 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 { @@ -43,6 +47,14 @@ export interface Insight { generatedBy: string; } +export interface QuantityTop { + code: string; + name: string; + pe: string; + pct: number; + thisMonth: number; +} + export interface ManComSnapshot { asOf: string; kpis: ManComKpis; @@ -51,6 +63,8 @@ export interface ManComSnapshot { 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. */ 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; +} From 3b241aa9c6c5dfd5fba980cdc3aa08eccadad15c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:34:06 +0000 Subject: [PATCH 36/68] feat(engineering/quantity): weekly accomplishment entry that auto-rolls to monthly report; per-project rollup (to-date %, this-month); monthly projection + material requirements (GDrive-sheet replacement); read-models + seed from real DPWH data https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/src/modules/engineering/entities.ts | 12 + .../src/modules/quantity/quantity.module.ts | 235 ++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 apps/api/src/modules/quantity/quantity.module.ts diff --git a/apps/api/src/modules/engineering/entities.ts b/apps/api/src/modules/engineering/entities.ts index 24e7776..4f0afc9 100644 --- a/apps/api/src/modules/engineering/entities.ts +++ b/apps/api/src/modules/engineering/entities.ts @@ -19,6 +19,18 @@ export class ProjectEntity extends BaseAppEntity { @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. */ 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 {} From ad90fe0f7108cf52d3eefb42f41034013b2c112e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:34:06 +0000 Subject: [PATCH 37/68] =?UTF-8?q?feat(ceo):=20incorporate=20real=20Quantit?= =?UTF-8?q?y=20accomplishment=20into=20ManCom=20(overall=20%,=20this-month?= =?UTF-8?q?=20=E2=82=B1,=20top-projects=20panel);=20wire=20QuantityModule?= =?UTF-8?q?=20+=20seed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/src/app.module.ts | 2 ++ apps/api/src/modules/ceo/ceo.module.ts | 15 ++++++++++++++- apps/api/src/seed/seed.module.ts | 5 ++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index d5fb80f..615703a 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -11,6 +11,7 @@ import { ItModule } from './modules/it/it.module'; import { OperationsModule } from './modules/operations/operations.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'; @@ -54,6 +55,7 @@ import { TicketingModule } from './shared/ticketing/ticketing.module'; ExpiryModule, // Department modules (breadth-first; deep work on module branches). EngineeringModule, + QuantityModule, ProcurementModule, OperationsModule, PropertyModule, diff --git a/apps/api/src/modules/ceo/ceo.module.ts b/apps/api/src/modules/ceo/ceo.module.ts index 9703786..ec25353 100644 --- a/apps/api/src/modules/ceo/ceo.module.ts +++ b/apps/api/src/modules/ceo/ceo.module.ts @@ -8,6 +8,7 @@ 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'; /** @@ -20,6 +21,7 @@ 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, ) {} @@ -57,6 +59,14 @@ export class CeoService { ? 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: { @@ -70,12 +80,15 @@ export class CeoService { 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, }; } @@ -106,7 +119,7 @@ export class CeoController { } @Module({ - imports: [EngineeringModule], + imports: [EngineeringModule, QuantityModule], controllers: [CeoController], providers: [CeoService], }) diff --git a/apps/api/src/seed/seed.module.ts b/apps/api/src/seed/seed.module.ts index c0273cf..a942cda 100644 --- a/apps/api/src/seed/seed.module.ts +++ b/apps/api/src/seed/seed.module.ts @@ -12,6 +12,7 @@ import { EngineeringModule } from '../modules/engineering/engineering.module'; import { PayItemEntity, ProjectEntity } from '../modules/engineering/entities'; import { ItModule, ItService } from '../modules/it/it.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 { ExpiryService } from '../shared/expiry/expiry.module'; @@ -28,6 +29,7 @@ export class SeedService implements OnApplicationBootstrap { private readonly property: PropertyService, private readonly records: RecordsService, private readonly it: ItService, + private readonly quantity: QuantityService, @InjectRepository(PayItemEntity) private readonly payItems: Repository, @InjectRepository(ProjectEntity) private readonly projects: Repository, @InjectRepository(InsightEntity) private readonly insights: Repository, @@ -142,13 +144,14 @@ export class SeedService implements OnApplicationBootstrap { await this.property.seed('seed'); await this.records.seed('seed'); await this.it.seed(ceoClaims); + await this.quantity.seed(); this.log.log('seed complete — ceo@ubi.ph/ceo123, admin/vpo/pm/pe, + dept heads head-@ubi.ph/head123'); } } @Module({ - imports: [DocTrackingModule, EngineeringModule, PropertyModule, RecordsModule, ItModule], + imports: [DocTrackingModule, EngineeringModule, PropertyModule, RecordsModule, ItModule, QuantityModule], providers: [SeedService], }) export class SeedModule {} From 49532a3ed6eaa77c89063dd940a5b8e9dc35aecc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:34:06 +0000 Subject: [PATCH 38/68] feat(web/quantity): simple weekly entry (one number/project/week), projection+materials grid (GDrive replacement), Quantity head dashboard; ManCom accomplishment KPI + quantity panel; Engineering links https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/App.tsx | 6 + apps/web/src/pages/Engineering.tsx | 6 + apps/web/src/pages/ManCom.tsx | 28 +++++ apps/web/src/pages/QuantityEntry.tsx | 100 ++++++++++++++++ apps/web/src/pages/QuantityHead.tsx | 56 +++++++++ apps/web/src/pages/QuantityProjection.tsx | 134 ++++++++++++++++++++++ apps/web/src/styles.css | 7 ++ 7 files changed, 337 insertions(+) create mode 100644 apps/web/src/pages/QuantityEntry.tsx create mode 100644 apps/web/src/pages/QuantityHead.tsx create mode 100644 apps/web/src/pages/QuantityProjection.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index c78a4d1..077210e 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -13,6 +13,9 @@ import Login from './pages/Login'; import ManCom from './pages/ManCom'; import PmsScheduler from './pages/PmsScheduler'; 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 }) { @@ -33,6 +36,9 @@ export default function App() { } /> } /> } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/apps/web/src/pages/Engineering.tsx b/apps/web/src/pages/Engineering.tsx index 9eadb9f..6b65d57 100644 --- a/apps/web/src/pages/Engineering.tsx +++ b/apps/web/src/pages/Engineering.tsx @@ -1,4 +1,5 @@ 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'; @@ -43,6 +44,11 @@ export default function Engineering() { return ( <> +
+ Quantity — weekly entry → + Quantity head dashboard → + Projection & materials → +
{error &&
{error}
}
diff --git a/apps/web/src/pages/ManCom.tsx b/apps/web/src/pages/ManCom.tsx index 623b8df..4f2b32d 100644 --- a/apps/web/src/pages/ManCom.tsx +++ b/apps/web/src/pages/ManCom.tsx @@ -61,6 +61,14 @@ export default function ManCom() { label={`${k.billingDraftsSubmitted}/${k.billingDraftsTotal} drafts`} />
+
+
Accomplishment
+
+ {(k.accomplishmentPct * 100).toFixed(1)}% + ₱{(k.accomplishmentMonth / 1e6).toFixed(1)}M mo +
+
in-house, from QE weekly entries
+
Avg slippage
@@ -129,6 +137,26 @@ export default function ManCom() {
+
+
+ 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 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/styles.css b/apps/web/src/styles.css index 2d7faa0..5d378f8 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -350,6 +350,13 @@ code { font-family: var(--mono); font-size: 0.9em; letter-spacing: -0.2px; } .spread .right { margin-left: auto; } .mb { margin-bottom: 14px; } +/* ── 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; } From cd8ea40a4e6037caa1b4a6181e9a47ddc4f1eebc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:45:42 +0000 Subject: [PATCH 39/68] feat(equipment): weekly machinery/activity schedule + push to Omega (OAEC API, simulated ref when OAEC_BASE_URL unset); read-models https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- .../src/modules/equipment/equipment.module.ts | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 apps/api/src/modules/equipment/equipment.module.ts 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 {} From ed834dabe4171755f2809edf61073ffcc336455b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:45:42 +0000 Subject: [PATCH 40/68] =?UTF-8?q?feat(procurement):=20materials-schedule?= =?UTF-8?q?=20intake=20=E2=80=94=20approved=20(PE=E2=86=92PM=E2=86=92VPO)?= =?UTF-8?q?=20schedules=20surface=20as=20ready-to-buy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- .../modules/procurement/procurement.module.ts | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/apps/api/src/modules/procurement/procurement.module.ts b/apps/api/src/modules/procurement/procurement.module.ts index 192ab87..c880af8 100644 --- a/apps/api/src/modules/procurement/procurement.module.ts +++ b/apps/api/src/modules/procurement/procurement.module.ts @@ -14,6 +14,9 @@ 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 + @@ -65,6 +68,9 @@ class CaptureDrDto { 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, ) {} @@ -87,6 +93,41 @@ export class ProcurementService implements OnModuleInit { }, () => 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 { @@ -131,10 +172,18 @@ export class ProcurementController { performance(): Promise> { return this.svc.supplierPerformance(); } + + @Get('materials-intake') + intake(): ReturnType { + return this.svc.materialsIntake(); + } } @Module({ - imports: [TypeOrmModule.forFeature([DeliveryReceiptEntity])], + imports: [ + TypeOrmModule.forFeature([DeliveryReceiptEntity, MaterialsScheduleEntity, ProjectEntity]), + EngineeringModule, + ], controllers: [ProcurementController], providers: [ProcurementService], exports: [ProcurementService], From db5bde86b3f731ae820b93ec67b6c65408945b7c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:45:42 +0000 Subject: [PATCH 41/68] chore(api): wire EquipmentModule + seed a sample equipment schedule https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/src/app.module.ts | 2 ++ apps/api/src/seed/seed.module.ts | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 615703a..600e7ac 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -7,6 +7,7 @@ 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 { OperationsModule } from './modules/operations/operations.module'; import { ProcurementModule } from './modules/procurement/procurement.module'; @@ -56,6 +57,7 @@ import { TicketingModule } from './shared/ticketing/ticketing.module'; // Department modules (breadth-first; deep work on module branches). EngineeringModule, QuantityModule, + EquipmentModule, ProcurementModule, OperationsModule, PropertyModule, diff --git a/apps/api/src/seed/seed.module.ts b/apps/api/src/seed/seed.module.ts index a942cda..88e0b13 100644 --- a/apps/api/src/seed/seed.module.ts +++ b/apps/api/src/seed/seed.module.ts @@ -10,6 +10,7 @@ import { DocTrackingService } from '../pillars/doc-tracking/doc-tracking.service import { DEPARTMENTS } from '../modules/dept/dept.module'; 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 { PropertyModule, PropertyService } from '../modules/property/property.module'; import { QuantityModule, QuantityService } from '../modules/quantity/quantity.module'; @@ -30,6 +31,7 @@ export class SeedService implements OnApplicationBootstrap { private readonly records: RecordsService, private readonly it: ItService, private readonly quantity: QuantityService, + private readonly equipment: EquipmentService, @InjectRepository(PayItemEntity) private readonly payItems: Repository, @InjectRepository(ProjectEntity) private readonly projects: Repository, @InjectRepository(InsightEntity) private readonly insights: Repository, @@ -145,13 +147,15 @@ export class SeedService implements OnApplicationBootstrap { 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); this.log.log('seed complete — ceo@ubi.ph/ceo123, admin/vpo/pm/pe, + dept heads head-@ubi.ph/head123'); } } @Module({ - imports: [DocTrackingModule, EngineeringModule, PropertyModule, RecordsModule, ItModule, QuantityModule], + imports: [DocTrackingModule, EngineeringModule, PropertyModule, RecordsModule, ItModule, QuantityModule, EquipmentModule], providers: [SeedService], }) export class SeedModule {} From aeb254357ee74926ea43569bc4ea605b4126ee81 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:45:42 +0000 Subject: [PATCH 42/68] =?UTF-8?q?feat(web):=20Equipment=20schedule=20page?= =?UTF-8?q?=20(data=20entry=20=E2=86=92=20push=20to=20Omega)=20+=20Procure?= =?UTF-8?q?ment=20intake=20page;=20Engineering=20+=20dept=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/App.tsx | 4 ++ apps/web/src/pages/Dept.tsx | 1 + apps/web/src/pages/Engineering.tsx | 2 + apps/web/src/pages/Equipment.tsx | 106 +++++++++++++++++++++++++++++ apps/web/src/pages/Procurement.tsx | 65 ++++++++++++++++++ 5 files changed, 178 insertions(+) create mode 100644 apps/web/src/pages/Equipment.tsx create mode 100644 apps/web/src/pages/Procurement.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 077210e..98af004 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -6,12 +6,14 @@ 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 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 PmsScheduler from './pages/PmsScheduler'; +import Procurement from './pages/Procurement'; import Property from './pages/Property'; import QuantityEntry from './pages/QuantityEntry'; import QuantityHead from './pages/QuantityHead'; @@ -39,6 +41,8 @@ export default function App() { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/apps/web/src/pages/Dept.tsx b/apps/web/src/pages/Dept.tsx index 1b2de99..2cadaac 100644 --- a/apps/web/src/pages/Dept.tsx +++ b/apps/web/src/pages/Dept.tsx @@ -15,6 +15,7 @@ interface DeptDashboard { /** 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' }, diff --git a/apps/web/src/pages/Engineering.tsx b/apps/web/src/pages/Engineering.tsx index 6b65d57..b9999d5 100644 --- a/apps/web/src/pages/Engineering.tsx +++ b/apps/web/src/pages/Engineering.tsx @@ -48,6 +48,8 @@ export default function Engineering() { Quantity — weekly entry → Quantity head dashboard → Projection & materials → + Equipment schedule → Omega + Procurement intake →
{error &&
{error}
} 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/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
+
+
+ + ); +} From 921452f322068b66a45c088ac6715f4d2ab64992 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:58:34 +0000 Subject: [PATCH 43/68] =?UTF-8?q?feat(web/docs):=20register-new-transmitta?= =?UTF-8?q?l=20form=20(was=20lookup/transmit/receive=20only)=20=E2=80=94?= =?UTF-8?q?=20POST=20/docs=20then=20opens=20it=20ready=20to=20transmit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/pages/Docs.tsx | 48 +++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/apps/web/src/pages/Docs.tsx b/apps/web/src/pages/Docs.tsx index d4a57b4..1f2ffbc 100644 --- a/apps/web/src/pages/Docs.tsx +++ b/apps/web/src/pages/Docs.tsx @@ -9,6 +9,8 @@ export default function Docs() { 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(); }, []); @@ -28,6 +30,21 @@ export default function Docs() { 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 }) }); @@ -45,12 +62,33 @@ export default function Docs() { 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.
+
+ )} -
- setCode(e.target.value)} placeholder="Where is… (scan or type a code, e.g. DOC-2026-00001)" /> - -
{error &&
{error}
} {answer && ( From 515d7ed3b7970976779d42bf3d2b499824ca6907 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:58:34 +0000 Subject: [PATCH 44/68] feat(web/nav): surface Ask AI to all signed-in users (answers stay RBAC-scoped); ManCom/Insights remain exec-only https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/Shell.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/Shell.tsx b/apps/web/src/Shell.tsx index 83cd290..e69cd3e 100644 --- a/apps/web/src/Shell.tsx +++ b/apps/web/src/Shell.tsx @@ -53,11 +53,11 @@ export default function Shell() {
Office of the President
{item('/mancom', 'ManCom')} {item('/insights', 'AI Insights')} - {item('/ask', 'Ask AI')} )}
Work
+ {item('/ask', 'Ask AI')} {item('/approvals', 'Approvals')} {item('/docs', 'Documents')} {item('/helpdesk', 'Helpdesk')} From 30a9a58b090bd067177e629c11492bfa93aba0ed Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 01:01:36 +0000 Subject: [PATCH 45/68] feat(web): floating icon-only Ask-AI assistant docked bottom-right on every page (popover chat, typewriter + sources, RBAC-scoped) https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/AssistantFab.tsx | 83 +++++++++++++++++++++++++++++++++++ apps/web/src/Shell.tsx | 2 + apps/web/src/styles.css | 39 ++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 apps/web/src/AssistantFab.tsx 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 index e69cd3e..7fa88fa 100644 --- a/apps/web/src/Shell.tsx +++ b/apps/web/src/Shell.tsx @@ -1,5 +1,6 @@ import { NavLink, Outlet, useNavigate } from 'react-router-dom'; import { clearSession, getUser, hasClaim } from './api'; +import AssistantFab from './AssistantFab'; const departments = [ ['engineering', 'Engineering'], @@ -84,6 +85,7 @@ export default function Shell() {
+
); } diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 5d378f8..db939f5 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -350,6 +350,45 @@ code { font-family: var(--mono); font-size: 0.9em; letter-spacing: -0.2px; } .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); } From 592965239e286b25add42bf5e6d2369125c69693 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 01:15:27 +0000 Subject: [PATCH 46/68] =?UTF-8?q?ops(cd):=20Claude-driven=20deployment=20k?= =?UTF-8?q?it=20=E2=80=94=20DEPLOY.md=20playbook=20(deploy+fix+rollback),?= =?UTF-8?q?=20wake=20trigger=20(timer=20+=20webhook),=20status-markdown=20?= =?UTF-8?q?handshake,=20tool=20allowlist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server Claude (claude -p on the box) deploys the production branch and fixes issues itself; status flows back via committed ops/DEPLOY_STATUS.md. https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- CLAUDE.md | 11 ++++ ops/DEPLOY.md | 89 +++++++++++++++++++++++++++++++++ ops/DEPLOY_STATUS.md | 12 +++++ ops/README.md | 78 +++++++++++++++++++++++++++++ ops/RELEASE_NOTES.md | 14 ++++++ ops/deploy-box.settings.json | 34 +++++++++++++ ops/systemd/ubi-deploy.service | 14 ++++++ ops/systemd/ubi-deploy.timer | 12 +++++ ops/systemd/ubi-webhook.service | 22 ++++++++ ops/wake-claude-deploy.sh | 44 ++++++++++++++++ ops/webhook-receiver.mjs | 49 ++++++++++++++++++ 11 files changed, 379 insertions(+) create mode 100644 ops/DEPLOY.md create mode 100644 ops/DEPLOY_STATUS.md create mode 100644 ops/README.md create mode 100644 ops/RELEASE_NOTES.md create mode 100644 ops/deploy-box.settings.json create mode 100644 ops/systemd/ubi-deploy.service create mode 100644 ops/systemd/ubi-deploy.timer create mode 100644 ops/systemd/ubi-webhook.service create mode 100755 ops/wake-claude-deploy.sh create mode 100644 ops/webhook-receiver.mjs diff --git a/CLAUDE.md b/CLAUDE.md index c89c43d..18ac743 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,6 +55,17 @@ printed in this repo. Micro-commits, conventional scopes per §11. +### Deploying (Claude-driven CD — see `ops/README.md`) +- Dev happens on the feature branch → merged into **`kickoff-build`**. +- **"build production"** = Cloud Claude fast-forwards the **`production`** branch + to the reviewed `kickoff-build` tip and pushes it (noting any env/migration + steps in `ops/RELEASE_NOTES.md`). +- A trigger on the box (1-min systemd timer and/or GitHub webhook) wakes + **Server Claude** (`claude -p` reading `ops/DEPLOY.md`) which deploys, **fixes + issues or rolls back**, and pushes `ops/DEPLOY_STATUS.md` back. +- At session start, Cloud Claude should read `ops/DEPLOY_STATUS.md` and act on + its `for_cloud_claude:` line. + --- ## 1. What this is diff --git a/ops/DEPLOY.md b/ops/DEPLOY.md new file mode 100644 index 0000000..211200e --- /dev/null +++ b/ops/DEPLOY.md @@ -0,0 +1,89 @@ +# 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 the way it runs here (discover it): `pm2 restart ubi-api` **or** + `systemctl restart ubi-api`. Web is static (`apps/web/dist`) served by nginx — no + restart needed unless nginx config changed. + +## 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/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..63744cc --- /dev/null +++ b/ops/RELEASE_NOTES.md @@ -0,0 +1,14 @@ +# 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 +- (none) + +## 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..d220563 --- /dev/null +++ b/ops/deploy-box.settings.json @@ -0,0 +1,34 @@ +{ + "_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(pm2 restart ubi-api)", + "Bash(pm2 logs:*)", + "Bash(systemctl restart ubi-api)", + "Bash(systemctl status ubi-api)", + "Bash(journalctl -u ubi-api:*)", + "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/systemd/ubi-deploy.service b/ops/systemd/ubi-deploy.service new file mode 100644 index 0000000..20021bd --- /dev/null +++ b/ops/systemd/ubi-deploy.service @@ -0,0 +1,14 @@ +# 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 +ExecStart=/usr/bin/env bash ops/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..5e17655 --- /dev/null +++ b/ops/systemd/ubi-webhook.service @@ -0,0 +1,22 @@ +# 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 +ExecStart=/usr/bin/node ops/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..780f92f --- /dev/null +++ b/ops/wake-claude-deploy.sh @@ -0,0 +1,44 @@ +#!/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)" + +# --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)); From cfadafa39ef5ff06570ea93e977e4c2222bbcc1a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 02:52:34 +0000 Subject: [PATCH 47/68] =?UTF-8?q?fix(seed):=20always=20run=20idempotent=20?= =?UTF-8?q?module=20seeds=20=E2=80=94=20engineering/quantity/property/etc?= =?UTF-8?q?=20data=20now=20backfills=20onto=20an=20existing=20demo=20DB=20?= =?UTF-8?q?after=20a=20deploy=20(was=20skipped=20by=20early=20return=20whe?= =?UTF-8?q?n=20pay=5Fitems=20already=20existed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified: on a DB with projects but no quantity rows + zeroed in-house, reboot restores in-house amounts + weekly entries without dropping the DB. https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/src/seed/seed.module.ts | 158 +++++++++++++++---------------- 1 file changed, 78 insertions(+), 80 deletions(-) diff --git a/apps/api/src/seed/seed.module.ts b/apps/api/src/seed/seed.module.ts index 88e0b13..d910c9f 100644 --- a/apps/api/src/seed/seed.module.ts +++ b/apps/api/src/seed/seed.module.ts @@ -43,9 +43,9 @@ export class SeedService implements OnApplicationBootstrap { this.log.log('SEED_DEMO != true — skipping demo seed'); return; } - if ((await this.payItems.count()) > 0) return; - this.log.log('empty database — seeding demo data'); + // Accounts are idempotent (ensureUser returns existing) — run every boot so + // newly-added dept-head accounts also backfill onto an existing demo DB. await this.auth.ensureUser({ email: 'admin@ubi.ph', name: 'Suite Admin', password: 'admin123', claims: ['role:admin'] }); const ceo = await this.auth.ensureUser({ email: 'ceo@ubi.ph', name: 'E. Castillo', password: 'ceo123', claims: ['role:ceo'] }); await this.auth.ensureUser({ email: 'vpo@ubi.ph', name: 'VPO Office', password: 'vpo123', claims: ['role:vpo', 'dept:operations'] }); @@ -55,94 +55,92 @@ export class SeedService implements OnApplicationBootstrap { 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)', password: 'agent123', claims: ['agent:insights'] }); - - // Department-head accounts — one per department, each scoped to its dept. - // They land on their own standard department dashboard. for (const d of DEPARTMENTS) { await this.auth.ensureUser({ - email: d.headEmail, - name: d.headName, - password: 'head123', + email: d.headEmail, name: d.headName, password: 'head123', claims: ['role:manager', `dept:${d.slug}`], }); } + const ceoClaims: UserClaims = this.auth.toClaims(ceo); - 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' })), - ); + // 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', - }), - ), - ); + 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' })), - ); + 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 ceoClaims: UserClaims = this.auth.toClaims(ceo); - 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'); + 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'); + } - // Real department modules — assets, vehicle docs, physical index, IT. + // 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); From f6ad6e687736acc2f5aeb0495f133fcd08c8c20d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 03:52:14 +0000 Subject: [PATCH 48/68] fix(deploy-feedback): hide demo logins in prod builds, correct restart to ubi-edge, capture nginx PWA hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the loop on DEPLOY_STATUS for_cloud_claude: - Login demo chips gated on import.meta.env.DEV / VITE_SHOW_DEMO_LOGINS (prod login page no longer advertises working creds; verified absent from bundle) - DEPLOY.md §4 + allowlist use 'systemctl restart ubi-edge' (binds :8091) - ops/nginx.sample.conf documents the immutable-assets / no-cache-shell hardening https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/pages/Login.tsx | 6 ++++++ ops/DEPLOY.md | 8 +++++--- ops/RELEASE_NOTES.md | 10 +++++++++- ops/deploy-box.settings.json | 8 +++----- ops/nginx.sample.conf | 30 ++++++++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 ops/nginx.sample.conf diff --git a/apps/web/src/pages/Login.tsx b/apps/web/src/pages/Login.tsx index 912a46c..b465dea 100644 --- a/apps/web/src/pages/Login.tsx +++ b/apps/web/src/pages/Login.tsx @@ -16,6 +16,10 @@ const DEPT_HEADS = [ 'it', 'records', 'clinic', 'admin', 'hr', 'property', 'finance', ]; +// Demo autofill chips advertise working credentials — never show them on a +// production login page. Dev shows them; prod only if explicitly opted in. +const SHOW_DEMOS = import.meta.env.DEV || import.meta.env.VITE_SHOW_DEMO_LOGINS === 'true'; + export default function Login() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -83,6 +87,7 @@ export default function Login() {
RevA
+ {SHOW_DEMOS && (
Tap to autofill a demo login
@@ -101,6 +106,7 @@ export default function Login() {
+ )}
Offline-ready: after the first online sign-in this device keeps a secure session for field use. diff --git a/ops/DEPLOY.md b/ops/DEPLOY.md index 211200e..bc2f5c9 100644 --- a/ops/DEPLOY.md +++ b/ops/DEPLOY.md @@ -48,9 +48,11 @@ ## 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 the way it runs here (discover it): `pm2 restart ubi-api` **or** - `systemctl restart ubi-api`. Web is static (`apps/web/dist`) served by nginx — no - restart needed unless nginx config changed. +- 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"...}`. diff --git a/ops/RELEASE_NOTES.md b/ops/RELEASE_NOTES.md index 63744cc..cd97c92 100644 --- a/ops/RELEASE_NOTES.md +++ b/ops/RELEASE_NOTES.md @@ -5,7 +5,15 @@ > clear stale notes after they're applied. ## Pending for next deploy -- (none) +- (none — restart command, demo-chip gating, nginx sample applied to repo) + +## Addressed from last DEPLOY_STATUS for_cloud_claude (2026-06-15) +- ✅ 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 diff --git a/ops/deploy-box.settings.json b/ops/deploy-box.settings.json index d220563..21e359a 100644 --- a/ops/deploy-box.settings.json +++ b/ops/deploy-box.settings.json @@ -15,11 +15,9 @@ "Bash(npm install)", "Bash(npm run build*)", "Bash(npm run migration:run)", - "Bash(pm2 restart ubi-api)", - "Bash(pm2 logs:*)", - "Bash(systemctl restart ubi-api)", - "Bash(systemctl status ubi-api)", - "Bash(journalctl -u ubi-api:*)", + "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:*)" ], diff --git a/ops/nginx.sample.conf b/ops/nginx.sample.conf new file mode 100644 index 0000000..aaca382 --- /dev/null +++ b/ops/nginx.sample.conf @@ -0,0 +1,30 @@ +# 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; + + # 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; } +} From 439ef93a5c21dd5d9e5a75c1c3f0eaf1c269f4cd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 04:08:47 +0000 Subject: [PATCH 49/68] =?UTF-8?q?ops(cd):=20INSTALL-AUTOMATION.md=20?= =?UTF-8?q?=E2=80=94=20stand=20up=20webhook=20(instant)=20+=20timer=20(bac?= =?UTF-8?q?kstop),=20with=20security=20hardening=20+=20end-to-end=20verify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- ops/INSTALL-AUTOMATION.md | 71 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 ops/INSTALL-AUTOMATION.md diff --git a/ops/INSTALL-AUTOMATION.md b/ops/INSTALL-AUTOMATION.md new file mode 100644 index 0000000..3a5168a --- /dev/null +++ b/ops/INSTALL-AUTOMATION.md @@ -0,0 +1,71 @@ +# 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) +``` +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`. From 5797fc1f71526b57dc8fe137312a90d5e64ea833 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 04:29:27 +0000 Subject: [PATCH 50/68] chore(repo): relax node engines to >=20.19 (matches box; clears EBADENGINE warning flagged by deploy) https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- ops/RELEASE_NOTES.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ops/RELEASE_NOTES.md b/ops/RELEASE_NOTES.md index cd97c92..1b7dc3d 100644 --- a/ops/RELEASE_NOTES.md +++ b/ops/RELEASE_NOTES.md @@ -8,6 +8,10 @@ - (none — restart command, demo-chip gating, nginx sample applied to repo) ## 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. diff --git a/package.json b/package.json index 1181d7b..52f7527 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,6 @@ "typecheck": "npm run build:packages && npm run typecheck -w @ubi/api -w @ubi/web" }, "engines": { - "node": ">=22" + "node": ">=20.19" } } From 7cdcb915cd2185442021dff0461328d524ad8ed0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 04:33:55 +0000 Subject: [PATCH 51/68] =?UTF-8?q?fix(ops/cd):=20loop=20guard=20in=20repo?= =?UTF-8?q?=20=E2=80=94=20skip=20deploy=20when=20tip=20is=20our=20own=20op?= =?UTF-8?q?s(deploy)=20status=20commit;=20run=20triggers=20from=20/opt=20t?= =?UTF-8?q?o=20survive=20git=20reset=20(folds=20server's=20box-side=20fix?= =?UTF-8?q?=20upstream)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, each deploy's status-commit push re-fires the webhook/timer → redeploy → push → infinite loop. Guard runs for both triggers. https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- ops/INSTALL-AUTOMATION.md | 14 ++++++++++++++ ops/systemd/ubi-deploy.service | 4 +++- ops/systemd/ubi-webhook.service | 3 ++- ops/wake-claude-deploy.sh | 10 ++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/ops/INSTALL-AUTOMATION.md b/ops/INSTALL-AUTOMATION.md index 3a5168a..8c9e778 100644 --- a/ops/INSTALL-AUTOMATION.md +++ b/ops/INSTALL-AUTOMATION.md @@ -14,6 +14,20 @@ that user, and `claude` already authenticated for it. 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:`. diff --git a/ops/systemd/ubi-deploy.service b/ops/systemd/ubi-deploy.service index 20021bd..0c6e607 100644 --- a/ops/systemd/ubi-deploy.service +++ b/ops/systemd/ubi-deploy.service @@ -11,4 +11,6 @@ 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 -ExecStart=/usr/bin/env bash ops/wake-claude-deploy.sh +# 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-webhook.service b/ops/systemd/ubi-webhook.service index 5e17655..dd73ce8 100644 --- a/ops/systemd/ubi-webhook.service +++ b/ops/systemd/ubi-webhook.service @@ -14,7 +14,8 @@ 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 -ExecStart=/usr/bin/node ops/webhook-receiver.mjs +# 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 diff --git a/ops/wake-claude-deploy.sh b/ops/wake-claude-deploy.sh index 780f92f..d9ee9df 100755 --- a/ops/wake-claude-deploy.sh +++ b/ops/wake-claude-deploy.sh @@ -16,6 +16,16 @@ 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 From 039323d9906acb32d56c8a82b770866478bcfc3e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 04:39:35 +0000 Subject: [PATCH 52/68] feat(web/login): show demo tap-to-login chips by default for POC (flip off with VITE_HIDE_DEMO_LOGINS=true at real launch) https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/pages/Login.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/pages/Login.tsx b/apps/web/src/pages/Login.tsx index b465dea..082179e 100644 --- a/apps/web/src/pages/Login.tsx +++ b/apps/web/src/pages/Login.tsx @@ -16,9 +16,9 @@ const DEPT_HEADS = [ 'it', 'records', 'clinic', 'admin', 'hr', 'property', 'finance', ]; -// Demo autofill chips advertise working credentials — never show them on a -// production login page. Dev shows them; prod only if explicitly opted in. -const SHOW_DEMOS = import.meta.env.DEV || import.meta.env.VITE_SHOW_DEMO_LOGINS === 'true'; +// POC/demo phase: show the tap-to-login chips everywhere so anyone can demo +// (data is fake). The day this goes live for real, set VITE_HIDE_DEMO_LOGINS=true. +const SHOW_DEMOS = import.meta.env.VITE_HIDE_DEMO_LOGINS !== 'true'; export default function Login() { const [email, setEmail] = useState(''); From 1943688c5a19af98ffa48bad1c44d5cd754eb491 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 04:47:32 +0000 Subject: [PATCH 53/68] =?UTF-8?q?chore(web):=20block=20search=20engines=20?= =?UTF-8?q?for=20POC=20=E2=80=94=20robots.txt=20+=20noindex=20meta=20(buil?= =?UTF-8?q?d)=20+=20X-Robots-Tag=20note=20for=20nginx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/index.html | 1 + apps/web/public/robots.txt | 3 +++ ops/RELEASE_NOTES.md | 5 ++++- ops/nginx.sample.conf | 4 ++++ 4 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 apps/web/public/robots.txt diff --git a/apps/web/index.html b/apps/web/index.html index 12495f3..87190f6 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -4,6 +4,7 @@ + UBI Suite 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/ops/RELEASE_NOTES.md b/ops/RELEASE_NOTES.md index 1b7dc3d..32d0f2c 100644 --- a/ops/RELEASE_NOTES.md +++ b/ops/RELEASE_NOTES.md @@ -5,7 +5,10 @@ > clear stale notes after they're applied. ## Pending for next deploy -- (none — restart command, demo-chip gating, nginx sample applied to repo) +- **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 diff --git a/ops/nginx.sample.conf b/ops/nginx.sample.conf index aaca382..62da3b5 100644 --- a/ops/nginx.sample.conf +++ b/ops/nginx.sample.conf @@ -10,6 +10,10 @@ server { 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; } From f085850f053fd28e2d43e1ef85e66118699d90a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 05:42:09 +0000 Subject: [PATCH 54/68] chore(seed): CEO display name -> Car Go; ensureUser now syncs demo display names on boot https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/src/pillars/auth/auth.service.ts | 9 ++++++++- apps/api/src/seed/seed.module.ts | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/api/src/pillars/auth/auth.service.ts b/apps/api/src/pillars/auth/auth.service.ts index 3d24a09..9b70376 100644 --- a/apps/api/src/pillars/auth/auth.service.ts +++ b/apps/api/src/pillars/auth/auth.service.ts @@ -54,7 +54,14 @@ export class AuthService { entity?: 'UBI' | 'OMEGA'; }): Promise { const existing = await this.findByEmail(input.email); - if (existing) return existing; + if (existing) { + // Keep the demo display name in sync with the seed (idempotent). + if (input.name && existing.name !== input.name) { + existing.name = input.name; + return this.users.save(existing); + } + return existing; + } const user = this.users.create({ email: input.email.toLowerCase(), name: input.name, diff --git a/apps/api/src/seed/seed.module.ts b/apps/api/src/seed/seed.module.ts index d910c9f..1100530 100644 --- a/apps/api/src/seed/seed.module.ts +++ b/apps/api/src/seed/seed.module.ts @@ -47,7 +47,7 @@ export class SeedService implements OnApplicationBootstrap { // Accounts are idempotent (ensureUser returns existing) — run every boot so // newly-added dept-head accounts also backfill onto an existing demo DB. await this.auth.ensureUser({ email: 'admin@ubi.ph', name: 'Suite Admin', password: 'admin123', claims: ['role:admin'] }); - const ceo = await this.auth.ensureUser({ email: 'ceo@ubi.ph', name: 'E. Castillo', password: 'ceo123', claims: ['role:ceo'] }); + const ceo = await this.auth.ensureUser({ email: 'ceo@ubi.ph', name: 'Car Go', password: 'ceo123', claims: ['role:ceo'] }); await this.auth.ensureUser({ email: 'vpo@ubi.ph', name: 'VPO Office', password: 'vpo123', claims: ['role:vpo', 'dept:operations'] }); await this.auth.ensureUser({ email: 'pm@ubi.ph', name: 'PM Uy', password: 'pm123', claims: ['role:pm', 'dept:engineering'] }); await this.auth.ensureUser({ From 985e8a4d3eb61acb41592d5c4bf2c3889310932c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 06:10:34 +0000 Subject: [PATCH 55/68] chore(auth): rotate all account passwords (managed bcrypt hashes), hide demo login chips in prod; ensureUser force-rotates on boot Plaintexts not committed (hashes only). Demo tap-to-login is off on the live login page now that real data is in. https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/src/pillars/auth/auth.service.ts | 14 ++++++++++--- apps/api/src/seed/credentials.ts | 25 +++++++++++++++++++++++ apps/api/src/seed/seed.module.ts | 20 ++++++++++-------- apps/web/src/pages/Login.tsx | 6 +++--- 4 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 apps/api/src/seed/credentials.ts diff --git a/apps/api/src/pillars/auth/auth.service.ts b/apps/api/src/pillars/auth/auth.service.ts index 9b70376..5e6132f 100644 --- a/apps/api/src/pillars/auth/auth.service.ts +++ b/apps/api/src/pillars/auth/auth.service.ts @@ -49,23 +49,31 @@ export class AuthService { 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; - return this.users.save(existing); + changed = true; } - return existing; + // 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.password ? await bcrypt.hash(input.password, 10) : null, + passwordHash: input.passwordHash ?? (input.password ? await bcrypt.hash(input.password, 10) : null), claims: input.claims, isField: input.isField ?? false, entity: input.entity ?? 'UBI', diff --git a/apps/api/src/seed/credentials.ts b/apps/api/src/seed/credentials.ts new file mode 100644 index 0000000..c7b51c5 --- /dev/null +++ b/apps/api/src/seed/credentials.ts @@ -0,0 +1,25 @@ +// Managed account password hashes (bcrypt — one-way, safe to commit). +// Plaintexts are NOT stored here; they were shown once at rotation time. +// To rotate again: regenerate hashes and replace this map; the seed force-applies +// them on boot. (POC/pilot credential management — no password-change UI yet.) +export const CREDENTIAL_HASHES: Record = { + 'admin@ubi.ph': '$2a$10$t5VM4D8DGVFpOM3RvH7EQOcQ7T785y.lxSphwBHGlRUfUs9H6wzjm', + 'ceo@ubi.ph': '$2a$10$zuo0r9V/N82HW6rY1uoc9uU.HFowE6cAxvIzTPpuuSWlK6ttgr7vS', + 'vpo@ubi.ph': '$2a$10$FJwlt6zApU4cfEDoDbo3Aea/6W6RkTRSqcpRd7oCIokqm4ODxOIzq', + 'pm@ubi.ph': '$2a$10$qlcX7Cq2LGzJYHJFmzfW.u8C5bUiWfnbtLQaBJxNp6CUk.r6xxPU6', + 'pe@ubi.ph': '$2a$10$Omq5z9mXW31VeJAR8t.ivOF0sVUEhL2iaPPTpAzI4hyef2ph37D9e', + 'insights-agent@ubi.ph': '$2a$10$8UG3ac0oYVS901nBXjSWKuWrkzjywa2OIRQDVTGB/DeNqs1JbqJ7K', + 'head-engineering@ubi.ph': '$2a$10$pN1FD2bm.cnNE7q5AqG7lOQSKF8utPynKeESp7uzPRpDYFJ1EcWNm', + 'head-procurement@ubi.ph': '$2a$10$Adu4DeLjzsptZ9.CRA89qeIsrGVJDuraw3lRIr.msGsEbWYNHPZIK', + 'head-operations@ubi.ph': '$2a$10$0up7XgM0XqDjfLExgCALmeqCFONwpCGJPTY0CxQvRBOIxpXbxsdtq', + 'head-survey@ubi.ph': '$2a$10$IMocJU5Iz7ZgeIEuJtQonOHh0XM7Y0VLycKphbvjHfWQBY76HsGGq', + 'head-mqc@ubi.ph': '$2a$10$EFRKpD4.mh3oTZssWqJzGeNEhKbtCAcv4qsySL2V7h36RJly968qi', + 'head-audit@ubi.ph': '$2a$10$OK4k3lvEGf08PeCX89iKS.J37uC.LNhUpSFqA8YBHeBUyfofdJRTO', + 'head-it@ubi.ph': '$2a$10$LSq3metsAWXXUJ3Mf1tN1uDd0mNTtGiym1clQrs76e1DRkT0Dehqe', + 'head-records@ubi.ph': '$2a$10$dFzqxe0pe0p3aT5XKffVKOvUlAIpZXmqt01fhnj7hpfQJAtuhlPsW', + 'head-clinic@ubi.ph': '$2a$10$bOC1DMcNRu1YWS4eGyTUi.75UjevtQqfnHm8YTo7a4GPCmOWFZsZW', + 'head-admin@ubi.ph': '$2a$10$lLFfJsLBt8bVYUhHgddSXOWIEfGQPsYa2dSUuX6iiU78luXI.NtHa', + 'head-hr@ubi.ph': '$2a$10$v/lkzSKBuLcTZ7CuSOerNOSWlbF4xAi3Imm2NfKcZVcIYr91/Jsgy', + 'head-property@ubi.ph': '$2a$10$HMKqkW9Ci2pQgYo.jt.QlufCKatfzQ7mcl4CmgGADtnUdn.EBqMl2', + 'head-finance@ubi.ph': '$2a$10$EbdOy29IVWI5XY0NSk.nJOlQAxD0DMxFstxRm8k6yzd2oPsSfDZhq', +}; diff --git a/apps/api/src/seed/seed.module.ts b/apps/api/src/seed/seed.module.ts index 1100530..f860b92 100644 --- a/apps/api/src/seed/seed.module.ts +++ b/apps/api/src/seed/seed.module.ts @@ -8,6 +8,7 @@ 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'; @@ -44,20 +45,21 @@ export class SeedService implements OnApplicationBootstrap { return; } - // Accounts are idempotent (ensureUser returns existing) — run every boot so - // newly-added dept-head accounts also backfill onto an existing demo DB. - await this.auth.ensureUser({ email: 'admin@ubi.ph', name: 'Suite Admin', password: 'admin123', claims: ['role:admin'] }); - const ceo = await this.auth.ensureUser({ email: 'ceo@ubi.ph', name: 'Car Go', password: 'ceo123', claims: ['role:ceo'] }); - await this.auth.ensureUser({ email: 'vpo@ubi.ph', name: 'VPO Office', password: 'vpo123', claims: ['role:vpo', 'dept:operations'] }); - await this.auth.ensureUser({ email: 'pm@ubi.ph', name: 'PM Uy', password: 'pm123', claims: ['role:pm', 'dept:engineering'] }); + // 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', password: 'pe123', + 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)', password: 'agent123', claims: ['agent:insights'] }); + 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, password: 'head123', + email: d.headEmail, name: d.headName, passwordHash: h(d.headEmail), claims: ['role:manager', `dept:${d.slug}`], }); } diff --git a/apps/web/src/pages/Login.tsx b/apps/web/src/pages/Login.tsx index 082179e..aadc446 100644 --- a/apps/web/src/pages/Login.tsx +++ b/apps/web/src/pages/Login.tsx @@ -16,9 +16,9 @@ const DEPT_HEADS = [ 'it', 'records', 'clinic', 'admin', 'hr', 'property', 'finance', ]; -// POC/demo phase: show the tap-to-login chips everywhere so anyone can demo -// (data is fake). The day this goes live for real, set VITE_HIDE_DEMO_LOGINS=true. -const SHOW_DEMOS = import.meta.env.VITE_HIDE_DEMO_LOGINS !== 'true'; +// Real data now: demo tap-to-login chips are OFF in production (only shown in +// local dev, or if explicitly opted in with VITE_SHOW_DEMO_LOGINS=true). +const SHOW_DEMOS = import.meta.env.DEV || import.meta.env.VITE_SHOW_DEMO_LOGINS === 'true'; export default function Login() { const [email, setEmail] = useState(''); From 2694154b0ad35652dadcaa5a0637721f6e84f034 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 06:28:48 +0000 Subject: [PATCH 56/68] =?UTF-8?q?fix(web/login):=20hard-disable=20demo=20c?= =?UTF-8?q?hips=20in=20prod=20(repo-authoritative,=20no=20env=20override)?= =?UTF-8?q?=20=E2=80=94=20box=20VITE=5FSHOW=5FDEMO=5FLOGINS=20could=20re-e?= =?UTF-8?q?nable=20them?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/web/src/pages/Login.tsx | 6 +++--- ops/RELEASE_NOTES.md | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/web/src/pages/Login.tsx b/apps/web/src/pages/Login.tsx index aadc446..70f2418 100644 --- a/apps/web/src/pages/Login.tsx +++ b/apps/web/src/pages/Login.tsx @@ -16,9 +16,9 @@ const DEPT_HEADS = [ 'it', 'records', 'clinic', 'admin', 'hr', 'property', 'finance', ]; -// Real data now: demo tap-to-login chips are OFF in production (only shown in -// local dev, or if explicitly opted in with VITE_SHOW_DEMO_LOGINS=true). -const SHOW_DEMOS = import.meta.env.DEV || import.meta.env.VITE_SHOW_DEMO_LOGINS === 'true'; +// 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(''); diff --git a/ops/RELEASE_NOTES.md b/ops/RELEASE_NOTES.md index 32d0f2c..5418221 100644 --- a/ops/RELEASE_NOTES.md +++ b/ops/RELEASE_NOTES.md @@ -5,6 +5,10 @@ > clear stale notes after they're applied. ## Pending for next deploy +- **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), From e0b0bf9636e3ff6e743d949847e82523f7729087 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 06:42:13 +0000 Subject: [PATCH 57/68] chore(auth): set all account passwords to a shared pilot password (Ulticon1) via managed hash https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/src/seed/credentials.ts | 46 +++++++++++++++++--------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/apps/api/src/seed/credentials.ts b/apps/api/src/seed/credentials.ts index c7b51c5..dcae6dd 100644 --- a/apps/api/src/seed/credentials.ts +++ b/apps/api/src/seed/credentials.ts @@ -1,25 +1,27 @@ // Managed account password hashes (bcrypt — one-way, safe to commit). -// Plaintexts are NOT stored here; they were shown once at rotation time. -// To rotate again: regenerate hashes and replace this map; the seed force-applies -// them on boot. (POC/pilot credential management — no password-change UI yet.) +// 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': '$2a$10$t5VM4D8DGVFpOM3RvH7EQOcQ7T785y.lxSphwBHGlRUfUs9H6wzjm', - 'ceo@ubi.ph': '$2a$10$zuo0r9V/N82HW6rY1uoc9uU.HFowE6cAxvIzTPpuuSWlK6ttgr7vS', - 'vpo@ubi.ph': '$2a$10$FJwlt6zApU4cfEDoDbo3Aea/6W6RkTRSqcpRd7oCIokqm4ODxOIzq', - 'pm@ubi.ph': '$2a$10$qlcX7Cq2LGzJYHJFmzfW.u8C5bUiWfnbtLQaBJxNp6CUk.r6xxPU6', - 'pe@ubi.ph': '$2a$10$Omq5z9mXW31VeJAR8t.ivOF0sVUEhL2iaPPTpAzI4hyef2ph37D9e', - 'insights-agent@ubi.ph': '$2a$10$8UG3ac0oYVS901nBXjSWKuWrkzjywa2OIRQDVTGB/DeNqs1JbqJ7K', - 'head-engineering@ubi.ph': '$2a$10$pN1FD2bm.cnNE7q5AqG7lOQSKF8utPynKeESp7uzPRpDYFJ1EcWNm', - 'head-procurement@ubi.ph': '$2a$10$Adu4DeLjzsptZ9.CRA89qeIsrGVJDuraw3lRIr.msGsEbWYNHPZIK', - 'head-operations@ubi.ph': '$2a$10$0up7XgM0XqDjfLExgCALmeqCFONwpCGJPTY0CxQvRBOIxpXbxsdtq', - 'head-survey@ubi.ph': '$2a$10$IMocJU5Iz7ZgeIEuJtQonOHh0XM7Y0VLycKphbvjHfWQBY76HsGGq', - 'head-mqc@ubi.ph': '$2a$10$EFRKpD4.mh3oTZssWqJzGeNEhKbtCAcv4qsySL2V7h36RJly968qi', - 'head-audit@ubi.ph': '$2a$10$OK4k3lvEGf08PeCX89iKS.J37uC.LNhUpSFqA8YBHeBUyfofdJRTO', - 'head-it@ubi.ph': '$2a$10$LSq3metsAWXXUJ3Mf1tN1uDd0mNTtGiym1clQrs76e1DRkT0Dehqe', - 'head-records@ubi.ph': '$2a$10$dFzqxe0pe0p3aT5XKffVKOvUlAIpZXmqt01fhnj7hpfQJAtuhlPsW', - 'head-clinic@ubi.ph': '$2a$10$bOC1DMcNRu1YWS4eGyTUi.75UjevtQqfnHm8YTo7a4GPCmOWFZsZW', - 'head-admin@ubi.ph': '$2a$10$lLFfJsLBt8bVYUhHgddSXOWIEfGQPsYa2dSUuX6iiU78luXI.NtHa', - 'head-hr@ubi.ph': '$2a$10$v/lkzSKBuLcTZ7CuSOerNOSWlbF4xAi3Imm2NfKcZVcIYr91/Jsgy', - 'head-property@ubi.ph': '$2a$10$HMKqkW9Ci2pQgYo.jt.QlufCKatfzQ7mcl4CmgGADtnUdn.EBqMl2', - 'head-finance@ubi.ph': '$2a$10$EbdOy29IVWI5XY0NSk.nJOlQAxD0DMxFstxRm8k6yzd2oPsSfDZhq', + '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, }; From a906575fe1413a1eb22c96f73b59be465ca67f83 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 13:27:36 +0000 Subject: [PATCH 58/68] chore(claude): default model to opus for this project (was opening on fable each session) https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- .claude/settings.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .claude/settings.json 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" +} From 3eda0e1d40316140f98085fa58f3d5ec78e114af Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 08:09:23 +0000 Subject: [PATCH 59/68] =?UTF-8?q?feat(fleet):=20Cartrack=20Fleet=20integra?= =?UTF-8?q?tion=20=E2=80=94=20live=20GPS=20map=20(Leaflet),=20vehicle=20li?= =?UTF-8?q?st,=20trips/events/geofences/drivers/fuel/maintenance=20proxy?= =?UTF-8?q?=20+=20read-models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Env-driven Basic auth (CARTRACK_REGION/USER/PASS); demo fleet when unset. Verified live against PH account: 56 vehicles located, nested location.* parsed. https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- .env.example | 7 + apps/api/src/app.module.ts | 2 + apps/api/src/modules/fleet/fleet.module.ts | 217 +++++++++++++++++++++ apps/web/package.json | 3 + apps/web/src/App.tsx | 2 + apps/web/src/Shell.tsx | 1 + apps/web/src/pages/Fleet.tsx | 98 ++++++++++ apps/web/src/styles.css | 5 + ops/RELEASE_NOTES.md | 5 + package-lock.json | 51 ++++- 10 files changed, 390 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/modules/fleet/fleet.module.ts create mode 100644 apps/web/src/pages/Fleet.tsx diff --git a/.env.example b/.env.example index 7f76f86..1091eea 100644 --- a/.env.example +++ b/.env.example @@ -42,3 +42,10 @@ 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/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 600e7ac..fca0bb4 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -8,6 +8,7 @@ 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 { FleetModule } from './modules/fleet/fleet.module'; import { ItModule } from './modules/it/it.module'; import { OperationsModule } from './modules/operations/operations.module'; import { ProcurementModule } from './modules/procurement/procurement.module'; @@ -63,6 +64,7 @@ import { TicketingModule } from './shared/ticketing/ticketing.module'; PropertyModule, RecordsModule, ItModule, + FleetModule, DeptModule, StubsModule, CeoModule, 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..cf78a92 --- /dev/null +++ b/apps/api/src/modules/fleet/fleet.module.ts @@ -0,0 +1,217 @@ +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]) => `${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; + lat: number | null; + lng: number | null; + speed: number | null; + heading: number | null; + ignition: boolean | null; + address: string | null; + at: string | null; + /** true when this row is demo data because credentials aren't set. */ + demo?: boolean; +} + +@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(), + ); + } + + 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(); + return [ + { id: 'D1', registration: 'UBI-DT-114', lat: 8.4542, lng: 124.6319, speed: 42, heading: 90, ignition: true, address: 'Cagayan de Oro — Daang Maharlika', at: now, demo: true }, + { id: 'D2', registration: 'UBI-DT-118', lat: 8.2280, lng: 124.2452, speed: 0, heading: 0, ignition: false, address: 'Iligan — PKG-02 site yard', at: now, demo: true }, + { id: 'D3', registration: 'UBI-SV-09', lat: 7.1907, lng: 125.4553, speed: 67, heading: 180, ignition: true, address: 'Davao — Mawab-Maco bypass', at: now, demo: true }, + { id: 'D4', registration: 'UBI-BH-21', lat: 8.1500, lng: 125.1278, speed: 0, heading: 0, ignition: false, address: 'Bukidnon — PKG-11 yard', at: now, demo: true }, + ]; + } + + 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; + return { + id: String(r.vehicle_id ?? r.id ?? r.terminal_id ?? i), + registration: String(r.registration ?? r.reg ?? r.plate ?? '—'), + 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, + address: (loc.position_description ?? r.address ?? null) as string | null, + at: (loc.updated ?? r.event_ts ?? r.gps_time ?? null) as string | null, + }; + }); + } + + async live(): Promise { + if (!this.cartrack.configured) return this.demoFleet(); + const data = await this.cartrack.getFirst(['/vehicles/status', '/vehicle/status/location', '/vehicle/status']); + const rows = Array.isArray(data) ? data : ((data as { data?: unknown[] })?.data ?? []); + return this.normalize(rows as Array>); + } + + 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(); + return this.cartrack.get('/vehicles'); + } + + async trips(params: { from?: string; to?: string; vehicle?: string }): Promise { + if (!this.cartrack.configured) return { demo: true, trips: [] }; + return this.cartrack.get('/trips', { start_date: params.from, end_date: params.to, vehicle_id: params.vehicle }); + } + + async events(): Promise { + if (!this.cartrack.configured) return [{ demo: true, type: 'harsh-braking', registration: 'UBI-DT-114', at: new Date().toISOString() }]; + return this.cartrack.get('/vehicle/events'); + } + + async geofences(): Promise { + if (!this.cartrack.configured) return { demo: true, geofences: [] }; + return this.cartrack.get('/geofences'); + } + + async drivers(): Promise { + if (!this.cartrack.configured) return { demo: true, drivers: [] }; + return this.cartrack.get('/drivers'); + } + + async maintenance(): Promise { + if (!this.cartrack.configured) return { demo: true, maintenance: [] }; + return this.cartrack.get('/maintenance'); + } + + async fuel(): Promise { + if (!this.cartrack.configured) return { demo: true, fuel: [] }; + return this.cartrack.get('/fuel'); + } +} + +@Controller('fleet') +export class FleetController { + constructor(private readonly svc: FleetService) {} + + @Get('status') status(): { configured: boolean; region: string } { return this.svc.status(); } + @Get('live') live(): Promise { return this.svc.live(); } + @Get('vehicles') vehicles(): Promise { return this.svc.vehicles(); } + @Get('events') events(): Promise { return this.svc.events(); } + @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('fuel') fuel(): Promise { return this.svc.fuel(); } + @Get('trips') trips(@Query('from') from?: string, @Query('to') to?: string, @Query('vehicle') vehicle?: string): Promise { + return this.svc.trips({ from, to, vehicle }); + } + @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/web/package.json b/apps/web/package.json index ca93686..ec5adfa 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,9 +12,12 @@ "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": { diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 98af004..8084dac 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -7,6 +7,7 @@ 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'; @@ -47,6 +48,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> diff --git a/apps/web/src/Shell.tsx b/apps/web/src/Shell.tsx index 7fa88fa..b1dc548 100644 --- a/apps/web/src/Shell.tsx +++ b/apps/web/src/Shell.tsx @@ -62,6 +62,7 @@ export default function Shell() { {item('/approvals', 'Approvals')} {item('/docs', 'Documents')} {item('/helpdesk', 'Helpdesk')} + {item('/fleet', 'Fleet')}
Departments
{departments.map(([slug, label]) => item(`/dept/${slug}`, label, true))} diff --git a/apps/web/src/pages/Fleet.tsx b/apps/web/src/pages/Fleet.tsx new file mode 100644 index 0000000..035306f --- /dev/null +++ b/apps/web/src/pages/Fleet.tsx @@ -0,0 +1,98 @@ +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; lat: number | null; lng: number | null; + speed: number | null; heading: number | null; ignition: boolean | null; + address: string | null; at: string | null; demo?: boolean; +} + +function FitBounds({ pts }: { pts: [number, number][] }) { + const map = useMap(); + useEffect(() => { + if (pts.length) map.fitBounds(pts as [number, number][], { padding: [40, 40], maxZoom: 11 }); + }, [pts, map]); + return null; +} + +export default function Fleet() { + const [vehicles, setVehicles] = useState(null); + const [cfg, setCfg] = useState<{ configured: boolean; region: string } | null>(null); + const [sel, setSel] = useState(null); + const [error, setError] = useState(null); + const timer = useRef(); + + const load = () => api('/fleet/live').then(setVehicles).catch((e: Error) => setError(e.message)); + + useEffect(() => { + api<{ configured: boolean; region: string }>('/fleet/status').then(setCfg).catch(() => {}); + void load(); + // live refresh every 20s + timer.current = window.setInterval(load, 20000); + return () => window.clearInterval(timer.current); + }, []); + + 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] ?? [10.3, 123.9]; // PH default + + if (error) return <>
{error}
; + if (!vehicles) return <>; + + return ( + <> + + {cfg && !cfg.configured && ( +
+ Showing demo vehicles. Add CARTRACK_USER + CARTRACK_PASS (region {cfg.region}) to go live — the map then plots real positions. +
+ )} + +
+
+ + + + {located.map((v) => ( + setSel(v.id) }} + > + + {v.registration}
+ {v.ignition ? 'moving' : 'stopped'} · {v.speed ?? 0} km/h
+ {v.address ?? `${v.lat!.toFixed(4)}, ${v.lng!.toFixed(4)}`}
+ {v.at ? new Date(v.at).toLocaleString() : ''} +
+
+ ))} +
+
+ +
+
VehiclesSRC · fleet.live
+ + + + {(vehicles ?? []).map((v) => ( + setSel(v.id)} style={{ cursor: 'pointer', background: sel === v.id ? 'rgba(194,65,12,0.06)' : undefined }}> + + + + + + ))} + +
RegStatusSpeedWhere
{v.registration}{v.ignition ? 'moving' : 'stopped'}{v.speed ?? 0} km/h{v.address ?? (v.lat != null ? `${v.lat.toFixed(3)}, ${v.lng!.toFixed(3)}` : 'no fix')}
+
Auto-refreshes every 20s · {located.length}/{vehicles.length} located
+
+
+ + ); +} diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index db939f5..c76a245 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -315,6 +315,11 @@ code { font-family: var(--mono); font-size: 0.9em; letter-spacing: -0.2px; } .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; } } diff --git a/ops/RELEASE_NOTES.md b/ops/RELEASE_NOTES.md index 5418221..13b0776 100644 --- a/ops/RELEASE_NOTES.md +++ b/ops/RELEASE_NOTES.md @@ -5,6 +5,11 @@ > clear stale notes after they're applied. ## Pending for next deploy +- **Fleet (Cartrack) credentials** — to make the live map use real GPS, add to the + API `.env` on the box (NOT committed): `CARTRACK_REGION=ph`, `CARTRACK_USER=…`, + `CARTRACK_PASS=…` (owner supplies the values; user = Cartrack API Username, + pass = the 64-char API password). Without them the Fleet page serves a labeled + demo fleet. Restart `ubi-edge` after setting. - **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 diff --git a/package-lock.json b/package-lock.json index a4ef96b..ed992fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "apps/*" ], "engines": { - "node": ">=22" + "node": ">=20.19" } }, "apps/api": { @@ -50,9 +50,12 @@ "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": { @@ -3099,6 +3102,17 @@ "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", @@ -3767,6 +3781,12 @@ "@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", @@ -3791,6 +3811,15 @@ "@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", @@ -7019,6 +7048,12 @@ "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", @@ -7970,6 +8005,20 @@ "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", From c05e3846046f487a1e227fb7c8e7c6f8ff46db08 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 08:25:47 +0000 Subject: [PATCH 60/68] =?UTF-8?q?feat(fleet):=20full=20Cartrack=20feature?= =?UTF-8?q?=20set=20=E2=80=94=20tabs=20for=20live=20map,=20trips,=20driver?= =?UTF-8?q?=20events,=20alerts,=20geofences,=20maintenance;=20per-vehicle?= =?UTF-8?q?=20locate;=20enriched=20live=20(odometer/fuel/driver);=20ManCom?= =?UTF-8?q?=20live-fleet=20card?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All verified against the live PH account: 56 vehicles, 10 trips, driver-events, 10 alerts, 10 geofences, maintenance/licence list. Correct paths/params (events end<=midnight, notifications bracket-keys). https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- apps/api/src/modules/fleet/fleet.module.ts | 133 ++++++++++++----- apps/web/src/pages/Fleet.tsx | 160 ++++++++++++++------- apps/web/src/pages/ManCom.tsx | 19 +++ 3 files changed, 224 insertions(+), 88 deletions(-) diff --git a/apps/api/src/modules/fleet/fleet.module.ts b/apps/api/src/modules/fleet/fleet.module.ts index cf78a92..2d9d8cd 100644 --- a/apps/api/src/modules/fleet/fleet.module.ts +++ b/apps/api/src/modules/fleet/fleet.module.ts @@ -35,7 +35,7 @@ export class CartrackClient { /** 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]) => `${k}=${encodeURIComponent(String(v))}`).join('&') + ? '?' + 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() }, @@ -71,17 +71,26 @@ export class CartrackClient { 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( @@ -100,7 +109,11 @@ export class FleetService implements OnModuleInit { ); this.registry.register( { key: 'fleet.events', module: 'property', title: 'Fleet — driver-behaviour events', description: 'Harsh braking / speeding / harsh acceleration.' }, - () => this.events(), + () => 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(), ); } @@ -111,11 +124,13 @@ export class FleetService implements OnModuleInit { /** 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 [ - { id: 'D1', registration: 'UBI-DT-114', lat: 8.4542, lng: 124.6319, speed: 42, heading: 90, ignition: true, address: 'Cagayan de Oro — Daang Maharlika', at: now, demo: true }, - { id: 'D2', registration: 'UBI-DT-118', lat: 8.2280, lng: 124.2452, speed: 0, heading: 0, ignition: false, address: 'Iligan — PKG-02 site yard', at: now, demo: true }, - { id: 'D3', registration: 'UBI-SV-09', lat: 7.1907, lng: 125.4553, speed: 67, heading: 180, ignition: true, address: 'Davao — Mawab-Maco bypass', at: now, demo: true }, - { id: 'D4', registration: 'UBI-BH-21', lat: 8.1500, lng: 125.1278, speed: 0, heading: 0, ignition: false, address: 'Bukidnon — PKG-11 yard', at: now, demo: true }, + 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'), ]; } @@ -129,25 +144,50 @@ export class FleetService implements OnModuleInit { 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.getFirst(['/vehicles/status', '/vehicle/status/location', '/vehicle/status']); - const rows = Array.isArray(data) ? data : ((data as { data?: unknown[] })?.data ?? []); - return this.normalize(rows as Array>); + 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 { @@ -155,39 +195,63 @@ export class FleetService implements OnModuleInit { return this.cartrack.post(`/vehicle/${id}/locate`); } - async vehicles(): Promise { - if (!this.cartrack.configured) return this.demoFleet(); - return this.cartrack.get('/vehicles'); + 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; vehicle?: string }): Promise { - if (!this.cartrack.configured) return { demo: true, trips: [] }; - return this.cartrack.get('/trips', { start_date: params.from, end_date: params.to, vehicle_id: params.vehicle }); + 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); } - async events(): Promise { - if (!this.cartrack.configured) return [{ demo: true, type: 'harsh-braking', registration: 'UBI-DT-114', at: new Date().toISOString() }]; - return this.cartrack.get('/vehicle/events'); + /** 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); } - async geofences(): Promise { - if (!this.cartrack.configured) return { demo: true, geofences: [] }; - return this.cartrack.get('/geofences'); + /** 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 drivers(): Promise { - if (!this.cartrack.configured) return { demo: true, drivers: [] }; - return this.cartrack.get('/drivers'); + async geofences(): Promise>> { + if (!this.cartrack.configured) return []; + return this.rows(await this.cartrack.get('/geofences')); } - async maintenance(): Promise { - if (!this.cartrack.configured) return { demo: true, maintenance: [] }; - return this.cartrack.get('/maintenance'); + async drivers(): Promise>> { + if (!this.cartrack.configured) return []; + return this.rows(await this.cartrack.get('/drivers')); } - async fuel(): Promise { - if (!this.cartrack.configured) return { demo: true, fuel: [] }; - return this.cartrack.get('/fuel'); + /** 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); } } @@ -196,16 +260,15 @@ 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('events') events(): Promise { return this.svc.events(); } @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('fuel') fuel(): Promise { return this.svc.fuel(); } - @Get('trips') trips(@Query('from') from?: string, @Query('to') to?: string, @Query('vehicle') vehicle?: string): Promise { - return this.svc.trips({ from, to, vehicle }); - } + @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); } } diff --git a/apps/web/src/pages/Fleet.tsx b/apps/web/src/pages/Fleet.tsx index 035306f..d54129a 100644 --- a/apps/web/src/pages/Fleet.tsx +++ b/apps/web/src/pages/Fleet.tsx @@ -5,94 +5,148 @@ import { api } from '../api'; import { Loader, SheetBar } from '../ui'; interface LiveVehicle { - id: string; registration: string; lat: number | null; lng: number | null; + 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' | 'trips' | 'events' | 'alerts' | 'geofences' | 'maintenance'; + +const TABS: { key: Tab; label: string }[] = [ + { key: 'live', label: 'Live map' }, + { key: 'trips', label: 'Trips' }, + { key: 'events', label: 'Driver events' }, + { key: 'alerts', label: 'Alerts' }, + { key: 'geofences', label: 'Geofences' }, + { key: 'maintenance', label: 'Maintenance' }, +]; +const COLS: Partial> = { + trips: ['registration', 'start_timestamp', 'end_timestamp', 'trip_distance', 'trip_duration', 'start_location', 'end_location'], + events: ['registration', 'event_description', 'event_type', 'event_ts', 'speed', 'position_description'], + alerts: ['registration', 'name', 'trigger_description', 'notification_msg', 'event_ts', 'speed'], + 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 as [number, number][], { padding: [40, 40], maxZoom: 11 }); - }, [pts, map]); + 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 load = () => api('/fleet/live').then(setVehicles).catch((e: Error) => setError(e.message)); + 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 load(); - // live refresh every 20s - timer.current = window.setInterval(load, 20000); + void loadLive(); + timer.current = window.setInterval(loadLive, 20000); return () => window.clearInterval(timer.current); }, []); + useEffect(() => { + if (tab === 'live') return; + setData(null); + api(`/fleet/${tab === 'alerts' ? 'notifications' : tab}`).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] ?? [10.3, 123.9]; // PH default + 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}
; - if (!vehicles) return <>; return ( <> - + {cfg && !cfg.configured && ( -
- Showing demo vehicles. Add CARTRACK_USER + CARTRACK_PASS (region {cfg.region}) to go live — the map then plots real positions. -
+
Demo fleet shown. Set CARTRACK_USER/CARTRACK_PASS (region {cfg.region}) for live GPS.
)} -
-
- - - - {located.map((v) => ( - setSel(v.id) }} - > - - {v.registration}
- {v.ignition ? 'moving' : 'stopped'} · {v.speed ?? 0} km/h
- {v.address ?? `${v.lat!.toFixed(4)}, ${v.lng!.toFixed(4)}`}
- {v.at ? new Date(v.at).toLocaleString() : ''} -
-
- ))} -
-
+
+ {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 · {v.odometerKm?.toLocaleString() ?? '—'} km
+ {v.address ?? `${v.lat!.toFixed(4)}, ${v.lng!.toFixed(4)}`}
+ {v.at ? new Date(v.at).toLocaleString() : ''} +
+
+ ))} +
+
+
+
VehiclesSRC · fleet.live
+
+ + + + {(vehicles ?? []).map((v) => ( + setSel(v.id)} style={{ cursor: 'pointer', background: sel === v.id ? 'rgba(194,65,12,0.06)' : undefined }}> + + + + + + ))} + +
RegStatuskm/h
{v.registration}
{(v.address ?? '').slice(0, 28)}
0 ? 'st-ok' : 'st-warn'}`}>{v.ignition && (v.speed ?? 0) > 0 ? 'moving' : 'stopped'}{v.speed ?? 0}
+
+
Auto-refresh 20s · {located.length}/{vehicles.length} located
+
+
+ ) + ) : (
-
VehiclesSRC · fleet.live
- - - - {(vehicles ?? []).map((v) => ( - setSel(v.id)} style={{ cursor: 'pointer', background: sel === v.id ? 'rgba(194,65,12,0.06)' : undefined }}> - - - - - - ))} - -
RegStatusSpeedWhere
{v.registration}{v.ignition ? 'moving' : 'stopped'}{v.speed ?? 0} km/h{v.address ?? (v.lat != null ? `${v.lat.toFixed(3)}, ${v.lng!.toFixed(3)}` : 'no fix')}
-
Auto-refreshes every 20s · {located.length}/{vehicles.length} located
+
{TABS.find((t) => t.key === tab)!.label}SRC · fleet.{tab}
+ {!data ? : }
-
+ )} ); } diff --git a/apps/web/src/pages/ManCom.tsx b/apps/web/src/pages/ManCom.tsx index 4f2b32d..128d7fe 100644 --- a/apps/web/src/pages/ManCom.tsx +++ b/apps/web/src/pages/ManCom.tsx @@ -7,6 +7,24 @@ 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); @@ -203,6 +221,7 @@ export default function ManCom() {
))} +
Ask AI
From ae7aff5f4cb0ffc4f041ecfad03456385445b604 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 09:13:45 +0000 Subject: [PATCH 61/68] ops(release): apply Cartrack fleet env on box + restart (owner-authorized pilot creds; rotate after) https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- ops/RELEASE_NOTES.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ops/RELEASE_NOTES.md b/ops/RELEASE_NOTES.md index 13b0776..2d5dbff 100644 --- a/ops/RELEASE_NOTES.md +++ b/ops/RELEASE_NOTES.md @@ -5,11 +5,16 @@ > clear stale notes after they're applied. ## Pending for next deploy -- **Fleet (Cartrack) credentials** — to make the live map use real GPS, add to the - API `.env` on the box (NOT committed): `CARTRACK_REGION=ph`, `CARTRACK_USER=…`, - `CARTRACK_PASS=…` (owner supplies the values; user = Cartrack API Username, - pass = the 64-char API password). Without them the Fleet page serves a labeled - demo fleet. Restart `ubi-edge` after setting. +- **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 From a80abc508a4fe6bb7a96364c36c21fc00169b131 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 09:20:26 +0000 Subject: [PATCH 62/68] =?UTF-8?q?docs(ops):=20pre-production=20audit=20che?= =?UTF-8?q?cklist=20(rotate=20Cartrack=20key/PAT/passwords,=20migrations,?= =?UTF-8?q?=20backups=E2=80=A6);=20CLAUDE.md=20=C2=A70=20points=20to=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01SmE5jXnu5J2Di7wG6po7yM --- CLAUDE.md | 5 +++++ ops/PRE-PROD-AUDIT.md | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 ops/PRE-PROD-AUDIT.md diff --git a/CLAUDE.md b/CLAUDE.md index 18ac743..a878d48 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,6 +55,11 @@ printed in this repo. 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 → merged into **`kickoff-build`**. - **"build production"** = Cloud Claude fast-forwards the **`production`** branch 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.). From c2f83f6484c0c446b6b51fa70dbd260a6ef9f1ad Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 11:23:53 +0000 Subject: [PATCH 63/68] feat(survey+mqc+hardening): real Survey & MQC modules + helmet + migration infra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Production hardening** - Add helmet to main.ts — security headers on every response - Add apps/api/src/data-source.ts for TypeORM CLI migrations - Add migration:generate / migration:run / migration:revert scripts to package.json **Survey module** (was stub, now real DB-backed) - `survey_measurement` entity — station/chainage (STA 0+000), pay item, volume, type (cross-section/stakeout/original-ground/as-built), period - `survey_cross_section` entity — area at station, interval, computed volume - `survey.volumes` read-model now aggregates real DB rows → feeds ManCom - REST: GET /survey/volumes, GET/POST /survey/projects/:id/measurements, GET/POST /survey/projects/:id/cross-sections - 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** (was stub, now real DB-backed) - DPWH minimum testing rules table (311(1)a/b/c, 200, 201, 301) → GET /mqc/rules/:payItemNo - `mqc_test` entity — test type, result, cert ID, lab ref, billing ref - `mqc_pour_log` entity — station, date, mix design, volume, slump, air content - `mqc_material_cert` entity — cert no, billing ref, file path (StorageProvider) - `mqc.certs` read-model now pulls pending tests from DB → blocking-billing KPI - REST: GET/POST /mqc/projects/:id/tests|pours|certs, GET /mqc/rules/:item - 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 **Shell / routing** - Added /survey and /mqc routes in App.tsx - Added "Tools" group in sidebar with Survey tools + MQC tools links - Added tab-btn / badge-warn / .field CSS to styles.css https://claude.ai/code/session_013K35zHH4mCqU8qHwN9Ypzg --- AUDIT.md | 151 +++++---- CLAUDE.md | 51 +-- apps/api/package.json | 6 +- apps/api/src/app.module.ts | 4 + apps/api/src/data-source.ts | 23 ++ apps/api/src/main.ts | 2 + apps/api/src/modules/mqc/mqc.module.ts | 305 ++++++++++++++++++ apps/api/src/modules/stubs/stubs.module.ts | 37 +-- apps/api/src/modules/survey/survey.module.ts | 223 +++++++++++++ apps/api/src/seed/seed.module.ts | 57 +++- apps/web/src/App.tsx | 4 + apps/web/src/Shell.tsx | 4 + apps/web/src/pages/Mqc.tsx | 310 +++++++++++++++++++ apps/web/src/pages/Survey.tsx | 143 +++++++++ apps/web/src/styles.css | 13 + 15 files changed, 1191 insertions(+), 142 deletions(-) create mode 100644 apps/api/src/data-source.ts create mode 100644 apps/api/src/modules/mqc/mqc.module.ts create mode 100644 apps/api/src/modules/survey/survey.module.ts create mode 100644 apps/web/src/pages/Mqc.tsx create mode 100644 apps/web/src/pages/Survey.tsx diff --git a/AUDIT.md b/AUDIT.md index 592f536..d9d90ad 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -1,101 +1,88 @@ -# Feature Audit vs Original Spec — 2026-06-14 +# Feature Audit vs Original Spec — updated 2026-06-16 -> **Update (build-all pass):** Property, Records and IT promoted from stubs to -> real DB-backed modules — see "Build-all results" at the bottom. +> Tracks modules against `CLAUDE.md` Part II. +## Legend +✅ built · 🟡 stub / read-surface only · ❌ not started -Measured against `CLAUDE.md` Part II. Status of the modules you asked about -first (IT, Property, Records/Doc-Tracking), plus the cross-cutting -department-dashboard layer added this session. +--- -> **Live-site note:** `edge.ubi-as.com` was running an **older build** at audit -> time — `POST /api/public/helpdesk` returned 404, so the public helpdesk + -> Turnstile work was not yet deployed. Pull `kickoff-build` and rebuild on the -> box to get everything below. +## 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 | -## Legend -✅ built · 🟡 stub / read-surface only · ❌ not started +--- -## IT module +## Survey module — ✅ real (this session) | Spec feature | Status | Notes | |---|---|---| -| AI Helpdesk (landing, queue) | ✅ | Public no-signin form + Turnstile gate; feeds shared ticketing; IT dept dashboard shows the queue | -| Capacity planning & controls | 🟡 | `it.capacity` read-model with storage/transfer/DB/UPS rows + amber/red thresholds (stub data) | -| SNMP device monitoring + map | ❌ | not started | -| DeskGuard v2 (client OCR + KPIs) | ❌ | not started | -| Maintenance scheduler (PMS) | ❌ | not started | -| Environmental monitoring (DHT22/ESP32) | ❌ | not started | - -## Property module +| 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 | |---|---|---| -| Asset custody read surface | 🟡 | `property.assets` stub rows (QR, type, custodian, location, status) | -| QR scan → location/status/custodian update | ❌ | no scan flow, no `asset_movement` table | -| Monthly custodian attestation | ❌ | not started | -| Feed IT PMS scheduler | ❌ | depends on PMS | +| 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 | -## Records / Document Tracking +--- + +## IT module | Spec feature | Status | Notes | |---|---|---| -| Document tracking (Pillar 2) | ✅ | QR codes, scan transmit/receive, where-is + full history — real | -| Expiry/renewal reminders | ✅ | shared `ExpiryModule`; `records.expiry` read-model | -| Vehicle reg/insurance/stamp registry | ❌ | no `vehicle_doc` entity yet | -| Physical-location index (access-gated) | ❌ | not started | +| 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 | -## Cross-cutting: Department dashboards (NEW this session) -| Capability | Status | Notes | -|---|---|---| -| Department-head accounts | ✅ | `head-@ubi.ph` / `head123`, claims `role:manager` + `dept:` (demo seed) | -| Standard KPI dashboard (uniform all depts) | ✅ | 4 cards: Monitored items · Reporting · Open approvals · Needs attention — same shape everywhere | -| Per-dept field monitoring | ✅ | renders that dept's Pillar-3 read-model | -| CCTV thumbnail per dept | ✅ | tile per `CAM--01`; live snapshot when `CCTV_BASE_URL` set, else honest NO-SIGNAL placeholder | -| RBAC isolation | ✅ | dept head sees only their dept (403 otherwise); CEO/VPO/admin cross-cutting | +## Property module — ✅ real QR EAM +`asset` + `asset_movement` + `custodian_attestation` tables. Scan endpoint, monthly attestation, history. Tools at `/property`. -Endpoint: `GET /api/dept/:slug/dashboard` · registry in -`apps/api/src/modules/dept/dept.module.ts`. +## Records module — ✅ real +`vehicle_doc` + expiry engine + `physical_location` index (access-gated by claim). Tools at `/records`. -## Suggested next build order -1. **Property QR EAM** — `asset` + `asset_movement` + scan endpoints + monthly - attestation (branch `property`). -2. **Records** — `vehicle_doc` registry + access-gated physical-location index - (branch `records`). -3. **IT depth** — PMS scheduler (pulls Property assets), then SNMP map, then - DeskGuard v2 (branch `it`). -4. **CCTV** — set `CCTV_BASE_URL` to the Hikvision NVR snapshot base so every - dept tile goes live. +## 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. --- -## Build-all results — 2026-06-14 (this session) - -### Property — ✅ real QR EAM -`asset` + `asset_movement` + `custodian_attestation` tables. Endpoints: -scan (`POST /property/assets/:qr/scan` — updates location/status/custodian + -logs movement), monthly attestation, full movement history. Tools page -`/property`. Active assets feed the IT PMS scheduler. - -### Records — ✅ real -`vehicle_doc` registry (reg/insurance/stamp) auto-mirrors into the shared -expiry engine; `records.expiry` now DB-backed. `physical_location` index is -**access-gated per row** — restricted titles (e.g. land titles → `role:legal`) -are masked unless the caller holds the claim (verified: records head sees -dept rows, legal row stays locked; IT head sees all locked). Tools page -`/records`. - -### IT — ✅ depth added -`it_device` (SNMP targets: UPS/switch/NVR/firewall), `it_pms` (quarterly, -**auto-generated from Property assets**), `deskguard_entry` (self-report + -mandatory consent banner surfaced in UI), `env_reading` (DHT22/IPMI temp). -Read-models: it.capacity, it.devices, it.pms, it.deskguard, it.env. Control -room page `/it`. - -### Cross-cutting -- 21 Pillar-3 read-models registered (was 15) — all flow into dept dashboards, - ManCom and Ask AI. -- Login page: clickable demo-login chips (autofill) + dept-head selector. - -### Still stub / not started -- IT SNMP **live polling** + topology map (device list is real, polling is not). -- Survey, MQC, Audit, Clinic, Admin, HR, Finance remain read-surface stubs - (render in dept dashboards; deep workflows pending per module branch). -- CCTV needs `CCTV_BASE_URL` → Hikvision NVR to go live. +## 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 index a878d48..c7c6f58 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,31 +8,33 @@ --- -## 0. CURRENT BUILD STATE — START HERE (updated 2026-06-12) +## 0. CURRENT BUILD STATE — START HERE (updated 2026-06-16) > **For Claude Code sessions:** the suite is **no longer pre-code**. A working -> breadth-first build lives on branch **`kickoff-build`** (PR #1 → `main`). -> If you cloned `main` and see only docs, run `git checkout kickoff-build` -> first. A deployment is live at `edge.ubi-as.com`. +> breadth-first build lives on PR #1 → `main` (branch `claude/magical-darwin-dulx3f` +> or `claude/gifted-heisenberg-inxyvn` — both are the same tip). +> A deployment is live at `edge.ubi-as.com`. **What exists and runs** -- Monorepo: `apps/api` (NestJS), `apps/web` (React+Vite PWA), +- 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 — 15 surfaces, `/api/ai/ask`, off-box insight + 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). -- **Modules:** Engineering core (projects, DPWH pay-item library, AI-prefilled - weekly materials schedule → approvals), Procurement DR capture, Operations - control tower, CEO ManCom consolidation (`/api/ceo/mancom`); every other - department exposes a read-surface stub registered in the AI layer. -- **Web:** "drawing comes alive" design system (title-block headers, stamps, - chainage rulers — `mockups.html` is the approved reference). Pages: login, - ManCom, AI Insights, Ask AI, Documents, Approvals, Engineering, - per-department read surfaces. +- **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), Fleet (Cartrack GPS/trips/events). +- **Web:** "drawing comes alive" design system. Pages: login, ManCom, AI + Insights, Ask AI, Documents, Approvals, Engineering, Quantity, Equipment, + Survey, MQC, Property, Records, IT (+ PMS), Fleet, per-department read surfaces. **Run locally** — see README. Short: `cp .env.example .env` → Postgres+Redis up → `npm install` → `npm run dev:api` (:3000; seeds demo data only when @@ -40,18 +42,19 @@ up → `npm install` → `npm run dev:api` (:3000; seeds demo data only when **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`. If the live DB was seeded with demo -users, **rotate or disable them immediately** — the demo passwords are -printed in this repo. +`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. Finish production hardening: TypeORM migrations to replace `DB_SYNC`, - helmet, nginx/systemd/deploy docs, Postgres + uploads backups. -2. Real SWA/billing tables behind `engineering.swa`; field photo capture - pipeline (GPS + timestamp + <500 KB compress + offline queue); Google - OAuth; offline cached login. -3. Deep module work on its own branch per §11 (`survey`, `mqc`, `it`, …), - keeping each module's Pillar-3 read model as the contract. +1. **Postgres + uploads backup script** — cron/systemd + `pg_dump` + rsync. +2. **Engineering SWA + billing** — real `weekly_swa` and `billing_batch` + tables; SWA feeds ManCom volumes. +3. **Audit module** — findings register + exception tracker feeds AI Insights. +4. **IT SNMP live polling** — actual SNMP worker + topology map. +5. **Field photo pipeline** — client-side compress + GPS/timestamp + offline + queue service worker. +6. **Google OAuth** — layer on top of standalone auth. Micro-commits, conventional scopes per §11. diff --git a/apps/api/package.json b/apps/api/package.json index 5954bc4..2ef4083 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -6,7 +6,10 @@ "build": "nest build", "start": "node dist/main.js", "start:dev": "nest start --watch", - "typecheck": "tsc -p tsconfig.json --noEmit" + "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", @@ -20,6 +23,7 @@ "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", diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index fca0bb4..4c27b4e 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -10,7 +10,9 @@ import { EngineeringModule } from './modules/engineering/engineering.module'; import { EquipmentModule } from './modules/equipment/equipment.module'; import { FleetModule } from './modules/fleet/fleet.module'; import { ItModule } from './modules/it/it.module'; +import { MqcModule } from './modules/mqc/mqc.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'; @@ -65,6 +67,8 @@ import { TicketingModule } from './shared/ticketing/ticketing.module'; RecordsModule, ItModule, FleetModule, + SurveyModule, + MqcModule, DeptModule, StubsModule, CeoModule, 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/main.ts b/apps/api/src/main.ts index fb62b33..696510b 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,10 +1,12 @@ 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] 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/stubs/stubs.module.ts b/apps/api/src/modules/stubs/stubs.module.ts index ffd3841..98a44ce 100644 --- a/apps/api/src/modules/stubs/stubs.module.ts +++ b/apps/api/src/modules/stubs/stubs.module.ts @@ -1,5 +1,5 @@ import { Controller, Get, Injectable, Module, OnModuleInit } from '@nestjs/common'; -import type { ReportStatus, TrendPoint } from '@ubi/types'; +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'; @@ -23,36 +23,7 @@ export class StubReadModelsService implements OnModuleInit { ) {} onModuleInit(): void { - this.registry.register( - { - key: 'survey.volumes', - module: 'survey', - title: 'Monthly survey volumes (m³ ×1000)', - description: 'Feeds ManCom consolidation; station/chainage indexed.', - }, - (): TrendPoint[] => [ - { period: 'Jan', plan: null, actual: 44, projected: null }, - { period: 'Feb', plan: null, actual: 54, projected: null }, - { period: 'Mar', plan: null, actual: 59, projected: null }, - { period: 'Apr', plan: null, actual: 50, projected: null }, - { period: 'May', plan: null, actual: 64, projected: null }, - { period: 'Jun', plan: null, actual: 31, projected: null }, - { period: 'Jul', plan: null, actual: null, projected: 61 }, - ], - ); - - this.registry.register( - { - key: 'mqc.certs', - module: 'mqc', - title: 'Material certificates queue', - description: 'DPWH minimum testing per pay item; certs attach to billings.', - }, - () => [ - { certId: 'MQC-2026-0712', payItemNo: '311(1)c', test: 'Concrete beam (7-day)', status: 'pending', daysPending: 6, billing: 'Billing No. 7 / PKG-02' }, - { certId: 'MQC-2026-0713', payItemNo: '311(1)c', test: 'Concrete beam (14-day)', status: 'pending', daysPending: 6, billing: 'Billing No. 7 / PKG-02' }, - ], - ); + // survey.volumes and mqc.certs are now owned by SurveyModule and MqcModule. this.registry.register( { @@ -136,9 +107,7 @@ export class StubsController { : { error: `read model ${key} not registered` }; } - // Real modules (it, records, property) own their own routes now. - @Get('survey/volumes') survey(): unknown { return this.query('survey.volumes'); } - @Get('mqc/certs') mqc(): unknown { return this.query('mqc.certs'); } + // 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'); } 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/seed/seed.module.ts b/apps/api/src/seed/seed.module.ts index f860b92..8ec8d6e 100644 --- a/apps/api/src/seed/seed.module.ts +++ b/apps/api/src/seed/seed.module.ts @@ -13,9 +13,11 @@ 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 { 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. */ @@ -33,6 +35,8 @@ export class SeedService implements OnApplicationBootstrap { private readonly it: ItService, private readonly quantity: QuantityService, private readonly equipment: EquipmentService, + private readonly survey: SurveyService, + private readonly mqc: MqcService, @InjectRepository(PayItemEntity) private readonly payItems: Repository, @InjectRepository(ProjectEntity) private readonly projects: Repository, @InjectRepository(InsightEntity) private readonly insights: Repository, @@ -150,12 +154,63 @@ export class SeedService implements OnApplicationBootstrap { 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(); + 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], + imports: [DocTrackingModule, EngineeringModule, PropertyModule, RecordsModule, ItModule, QuantityModule, EquipmentModule, SurveyModule, MqcModule], providers: [SeedService], }) export class SeedModule {} diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 8084dac..036ee39 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -13,8 +13,10 @@ 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'; @@ -48,6 +50,8 @@ export default function App() { } /> } /> } /> + } /> + } /> } /> } /> diff --git a/apps/web/src/Shell.tsx b/apps/web/src/Shell.tsx index b1dc548..0285a6a 100644 --- a/apps/web/src/Shell.tsx +++ b/apps/web/src/Shell.tsx @@ -64,6 +64,10 @@ export default function Shell() { {item('/helpdesk', 'Helpdesk')} {item('/fleet', 'Fleet')} +
Tools
+ {item('/survey', 'Survey tools')} + {item('/mqc', 'MQC tools')} +
Departments
{departments.map(([slug, label]) => item(`/dept/${slug}`, label, true))} 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/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 index c76a245..a590ad4 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -460,3 +460,16 @@ code { font-family: var(--mono); font-size: 0.9em; letter-spacing: -0.2px; } .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; } From 8b3851c66372b6dae10db3fa17a8e7a7c4ad1837 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 11:30:49 +0000 Subject: [PATCH 64/68] ops(release): prep 2026-06-16 deploy notes (survey+mqc+helmet) https://claude.ai/code/session_013K35zHH4mCqU8qHwN9Ypzg --- ops/RELEASE_NOTES.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ops/RELEASE_NOTES.md b/ops/RELEASE_NOTES.md index 2d5dbff..0c8e97d 100644 --- a/ops/RELEASE_NOTES.md +++ b/ops/RELEASE_NOTES.md @@ -4,7 +4,22 @@ > 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 +## 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): ``` From 1b6aefa333e7566547abf777ce40dc3066e6fdcd Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 01:37:50 +0000 Subject: [PATCH 65/68] =?UTF-8?q?feat(omega):=20Omega=20module=20=E2=80=94?= =?UTF-8?q?=20server-side=20Cartrack=20poller=20+=20analytics=20DB=20+=20e?= =?UTF-8?q?quipment=20register?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Replaces FleetModule with OmegaModule** (fleet is a sub-module of Omega Asia). Fleet API paths unchanged (/api/fleet/...) for web compat; equipment register at /api/omega/... **Server-side polling (no more on-demand Cartrack calls per request):** - 10s: pull /vehicles/status → FleetCache (in-memory); live map reads from cache - 60s: save position snapshot → fleet_position_log (analytics / history replay) - 10min: pull /vehicles/events + /alerts/notifications → fleet_driver_event + fleet_alert - FleetPoller implements OnModuleDestroy — clears intervals cleanly on shutdown - Falls back to direct Cartrack call only if cache is stale (>30s, e.g. poller restart) - Demo fleet served when CARTRACK_USER/PASS not set **New DB entities:** - fleet_position_log — lat/lng/speed/ignition/odometer/fuel per vehicle per minute; auto-purges >30d - fleet_driver_event — harsh braking/speeding events from 10min poll (raw JSONB preserved) - fleet_alert — geofence/ignition/panic alerts; acknowledgement endpoint POST /fleet/notifications/:id/ack - omega_equipment — OAEC equipment register (unit no, plate, type, cartrack_id, status, project) **New API endpoints:** - GET /fleet/vehicles/:id/history?hours=24 — position replay from DB - GET /fleet/events?hours=24 — driver events from DB (not live Cartrack) - GET /fleet/notifications?unack=true — alerts from DB - POST /fleet/notifications/:id/ack — acknowledge alert - GET /omega/equipment — equipment register - POST /omega/equipment — upsert equipment record **Read-models registered:** fleet.live, fleet.summary, fleet.events, fleet.alerts, omega.equipment **Web:** - Client poll: 20s → 5s (feels real-time since server caches at 10s) - Equipment tab added (reads from /omega/equipment) - Events/alerts columns updated to DB field names - Sheet bar rebranded to "Omega Asia — Fleet & Equipment" - Sidebar: "Fleet" → "Omega / Fleet" - Seed: 6 OAEC equipment units (entity=OMEGA) https://claude.ai/code/session_013K35zHH4mCqU8qHwN9Ypzg --- apps/api/src/app.module.ts | 4 +- apps/api/src/modules/omega/omega.module.ts | 618 +++++++++++++++++++++ apps/api/src/seed/seed.module.ts | 5 +- apps/web/src/Shell.tsx | 2 +- apps/web/src/pages/Fleet.tsx | 21 +- 5 files changed, 639 insertions(+), 11 deletions(-) create mode 100644 apps/api/src/modules/omega/omega.module.ts diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 4c27b4e..cf1f2b7 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -8,9 +8,9 @@ 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 { FleetModule } from './modules/fleet/fleet.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'; @@ -66,7 +66,7 @@ import { TicketingModule } from './shared/ticketing/ticketing.module'; PropertyModule, RecordsModule, ItModule, - FleetModule, + OmegaModule, SurveyModule, MqcModule, DeptModule, 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..7d45ff4 --- /dev/null +++ b/apps/api/src/modules/omega/omega.module.ts @@ -0,0 +1,618 @@ +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 }), + ); + } + + 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 }); + } +} + +// ──────────────────────────── 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)); + } +} + +/** 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/seed/seed.module.ts b/apps/api/src/seed/seed.module.ts index 8ec8d6e..154ef84 100644 --- a/apps/api/src/seed/seed.module.ts +++ b/apps/api/src/seed/seed.module.ts @@ -14,6 +14,7 @@ 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'; @@ -37,6 +38,7 @@ export class SeedService implements OnApplicationBootstrap { 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, @@ -156,6 +158,7 @@ export class SeedService implements OnApplicationBootstrap { 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'); } @@ -210,7 +213,7 @@ export class SeedService implements OnApplicationBootstrap { } @Module({ - imports: [DocTrackingModule, EngineeringModule, PropertyModule, RecordsModule, ItModule, QuantityModule, EquipmentModule, SurveyModule, MqcModule], + imports: [DocTrackingModule, EngineeringModule, PropertyModule, RecordsModule, ItModule, QuantityModule, EquipmentModule, SurveyModule, MqcModule, OmegaModule], providers: [SeedService], }) export class SeedModule {} diff --git a/apps/web/src/Shell.tsx b/apps/web/src/Shell.tsx index 0285a6a..f9002a1 100644 --- a/apps/web/src/Shell.tsx +++ b/apps/web/src/Shell.tsx @@ -62,7 +62,7 @@ export default function Shell() { {item('/approvals', 'Approvals')} {item('/docs', 'Documents')} {item('/helpdesk', 'Helpdesk')} - {item('/fleet', 'Fleet')} + {item('/fleet', 'Omega / Fleet')}
Tools
{item('/survey', 'Survey tools')} diff --git a/apps/web/src/pages/Fleet.tsx b/apps/web/src/pages/Fleet.tsx index d54129a..e631375 100644 --- a/apps/web/src/pages/Fleet.tsx +++ b/apps/web/src/pages/Fleet.tsx @@ -11,10 +11,11 @@ interface LiveVehicle { address: string | null; at: string | null; demo?: boolean; } type Row = Record; -type Tab = 'live' | 'trips' | 'events' | 'alerts' | 'geofences' | 'maintenance'; +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' }, @@ -22,9 +23,10 @@ const TABS: { key: Tab; label: string }[] = [ { 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', 'event_description', 'event_type', 'event_ts', 'speed', 'position_description'], - alerts: ['registration', 'name', 'trigger_description', 'notification_msg', 'event_ts', 'speed'], + 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'], }; @@ -67,14 +69,19 @@ export default function Fleet() { useEffect(() => { api<{ configured: boolean; region: string }>('/fleet/status').then(setCfg).catch(() => {}); void loadLive(); - timer.current = window.setInterval(loadLive, 20000); + // 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); - api(`/fleet/${tab === 'alerts' ? 'notifications' : tab}`).then(setData).catch((e: Error) => setError(e.message)); + 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]); @@ -90,7 +97,7 @@ export default function Fleet() { return ( <> - + {cfg && !cfg.configured && (
Demo fleet shown. Set CARTRACK_USER/CARTRACK_PASS (region {cfg.region}) for live GPS.
)} @@ -137,7 +144,7 @@ export default function Fleet() {
-
Auto-refresh 20s · {located.length}/{vehicles.length} located
+
Client refresh 5s · server cache 10s · {located.length}/{vehicles.length} located
) From eebdd7d9e649fb9df90602497edf887ba4b48643 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 01:40:53 +0000 Subject: [PATCH 66/68] =?UTF-8?q?feat(omega/fleet):=20fuel=20level=20?= =?UTF-8?q?=E2=80=94=20display=20+=20low-fuel=20alerts=20+=20analytics=20t?= =?UTF-8?q?rend=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Live map popup: shows fuel %, driver name, low-fuel ⚠️ warning when <20% - Vehicle list: Fuel column with st-ok (≥40%) / st-warn (<40%) / st-bad (<20%) chips - GET /fleet/fuel/trend?hours=8 — fuel% over time per vehicle from fleet_position_log - fleet.fuel read-model registered → CEO Ask AI / ManCom can report low-fuel vehicles - FleetService.fuelTrend() aggregates stored position snapshots by vehicle https://claude.ai/code/session_013K35zHH4mCqU8qHwN9Ypzg --- apps/api/src/modules/omega/omega.module.ts | 33 ++++++++++++++++++++++ apps/web/src/pages/Fleet.tsx | 31 +++++++++++++------- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/apps/api/src/modules/omega/omega.module.ts b/apps/api/src/modules/omega/omega.module.ts index 7d45ff4..ef8a627 100644 --- a/apps/api/src/modules/omega/omega.module.ts +++ b/apps/api/src/modules/omega/omega.module.ts @@ -395,6 +395,13 @@ export class FleetService implements OnModuleInit { { 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[] { @@ -501,6 +508,25 @@ export class FleetService implements OnModuleInit { 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 ────────────────────── @@ -586,6 +612,13 @@ export class FleetController { 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/... */ diff --git a/apps/web/src/pages/Fleet.tsx b/apps/web/src/pages/Fleet.tsx index e631375..38ffb78 100644 --- a/apps/web/src/pages/Fleet.tsx +++ b/apps/web/src/pages/Fleet.tsx @@ -119,7 +119,10 @@ export default function Fleet() { eventHandlers={{ click: () => setSel(v.id) }}> {v.registration} {v.name ? `· ${v.name}` : ''}
- {v.ignition && (v.speed ?? 0) > 0 ? 'moving' : 'stopped'} · {v.speed ?? 0} km/h · {v.odometerKm?.toLocaleString() ?? '—'} km
+ {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() : ''}
@@ -131,16 +134,24 @@ export default function Fleet() {
VehiclesSRC · fleet.live
- + - {(vehicles ?? []).map((v) => ( - setSel(v.id)} style={{ cursor: 'pointer', background: sel === v.id ? 'rgba(194,65,12,0.06)' : undefined }}> - - - - - - ))} + {(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/h
RegStatuskm/hFuel
{v.registration}
{(v.address ?? '').slice(0, 28)}
0 ? 'st-ok' : 'st-warn'}`}>{v.ignition && (v.speed ?? 0) > 0 ? 'moving' : 'stopped'}{v.speed ?? 0}
{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)}% + : } +
From d56c42e6682092892ddb86a861859ab59b776410 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 05:51:17 +0000 Subject: [PATCH 67/68] =?UTF-8?q?docs(claude):=20clarify=20CD=20chain=20?= =?UTF-8?q?=E2=80=94=20production=20branch=20=3D=20deploy=20signal,=20Serv?= =?UTF-8?q?er=20Claude=20polls=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c7c6f58..7f3429c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,13 +64,17 @@ Micro-commits, conventional scopes per §11. > If the owner mentions "audit before production," raise that checklist. ### Deploying (Claude-driven CD — see `ops/README.md`) -- Dev happens on the feature branch → merged into **`kickoff-build`**. -- **"build production"** = Cloud Claude fast-forwards the **`production`** branch - to the reviewed `kickoff-build` tip and pushes it (noting any env/migration - steps in `ops/RELEASE_NOTES.md`). -- A trigger on the box (1-min systemd timer and/or GitHub webhook) wakes - **Server Claude** (`claude -p` reading `ops/DEPLOY.md`) which deploys, **fixes - issues or rolls back**, and pushes `ops/DEPLOY_STATUS.md` back. +- 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. From 8430409e5a9d1175429ab4077f83dfd57b31c6c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 03:30:17 +0000 Subject: [PATCH 68/68] =?UTF-8?q?docs(handoff):=20session=20state=202026-0?= =?UTF-8?q?6-18=20=E2=80=94=20Omega=20on=20production=20branch,=20deploy?= =?UTF-8?q?=20in=20progress,=20DNS=20issue=20noted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 56 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7f3429c..1ab8175 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,12 +8,34 @@ --- -## 0. CURRENT BUILD STATE — START HERE (updated 2026-06-16) - -> **For Claude Code sessions:** the suite is **no longer pre-code**. A working -> breadth-first build lives on PR #1 → `main` (branch `claude/magical-darwin-dulx3f` -> or `claude/gifted-heisenberg-inxyvn` — both are the same tip). -> A deployment is live at `edge.ubi-as.com`. +## 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), @@ -31,10 +53,12 @@ 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), Fleet (Cartrack GPS/trips/events). + 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), Fleet, per-department read surfaces. + 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 @@ -47,14 +71,18 @@ migrations (`npm run migration:generate` then `migration:run`) and set `DB_SYNC=false`. See `ops/PRE-PROD-AUDIT.md`. **Next work, in order** -1. **Postgres + uploads backup script** — cron/systemd + `pg_dump` + rsync. -2. **Engineering SWA + billing** — real `weekly_swa` and `billing_batch` +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. -3. **Audit module** — findings register + exception tracker feeds AI Insights. -4. **IT SNMP live polling** — actual SNMP worker + topology map. -5. **Field photo pipeline** — client-side compress + GPS/timestamp + offline +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. -6. **Google OAuth** — layer on top of standalone auth. +8. **Google OAuth** — layer on top of standalone auth. Micro-commits, conventional scopes per §11.