From d95f11dd795edd49719977ac4b13347d5a60172e Mon Sep 17 00:00:00 2001 From: Tiger Ren Date: Sun, 1 Feb 2026 14:29:18 +0800 Subject: [PATCH] Initial commit --- .gitignore | 6 + README.md | 102 +- components.json | 23 + data/.gitkeep | 0 docs/FSD.md | 552 ++++ docs/TSD.md | 1163 ++++++++ drizzle.config.ts | 10 + next.config.ts | 8 +- package-lock.json | 2810 +++++++++++++++++++- package.json | 23 +- src/app/api/releases/[id]/route.ts | 23 + src/app/api/steps/[id]/route.ts | 18 + src/app/api/steps/route.ts | 28 + src/app/clusters/[id]/edit/page.tsx | 30 + src/app/clusters/[id]/page.tsx | 101 + src/app/clusters/new/page.tsx | 15 + src/app/clusters/page.tsx | 60 + src/app/customers/[id]/edit/page.tsx | 34 + src/app/customers/[id]/page.tsx | 136 + src/app/customers/new/page.tsx | 34 + src/app/customers/page.tsx | 75 + src/app/globals.css | 127 +- src/app/layout.tsx | 18 +- src/app/page.tsx | 290 +- src/app/releases/[id]/edit/page.tsx | 30 + src/app/releases/[id]/page.tsx | 363 +++ src/app/releases/[id]/steps/page.tsx | 235 ++ src/app/releases/new/page.tsx | 15 + src/app/releases/page.tsx | 89 + src/components/clusters/cluster-card.tsx | 101 + src/components/clusters/cluster-form.tsx | 113 + src/components/customers/customer-card.tsx | 99 + src/components/customers/customer-form.tsx | 143 + src/components/layout/sidebar.tsx | 68 + src/components/releases/release-card.tsx | 198 ++ src/components/releases/release-form.tsx | 163 ++ src/components/ui/alert-dialog.tsx | 196 ++ src/components/ui/alert.tsx | 66 + src/components/ui/badge.tsx | 48 + src/components/ui/button.tsx | 64 + src/components/ui/card.tsx | 92 + src/components/ui/collapsible.tsx | 33 + src/components/ui/dialog.tsx | 158 ++ src/components/ui/dropdown-menu.tsx | 257 ++ src/components/ui/input.tsx | 21 + src/components/ui/label.tsx | 24 + src/components/ui/scroll-area.tsx | 58 + src/components/ui/select.tsx | 190 ++ src/components/ui/separator.tsx | 28 + src/components/ui/table.tsx | 116 + src/components/ui/tabs.tsx | 91 + src/components/ui/textarea.tsx | 18 + src/lib/actions/clusters.ts | 79 + src/lib/actions/customer-steps.ts | 242 ++ src/lib/actions/customers.ts | 93 + src/lib/actions/releases.ts | 186 ++ src/lib/actions/step-templates.ts | 128 + src/lib/db/index.ts | 106 + src/lib/db/schema.ts | 140 + src/lib/utils.ts | 6 + 60 files changed, 9635 insertions(+), 108 deletions(-) create mode 100644 components.json create mode 100644 data/.gitkeep create mode 100644 docs/FSD.md create mode 100644 docs/TSD.md create mode 100644 drizzle.config.ts create mode 100644 src/app/api/releases/[id]/route.ts create mode 100644 src/app/api/steps/[id]/route.ts create mode 100644 src/app/api/steps/route.ts create mode 100644 src/app/clusters/[id]/edit/page.tsx create mode 100644 src/app/clusters/[id]/page.tsx create mode 100644 src/app/clusters/new/page.tsx create mode 100644 src/app/clusters/page.tsx create mode 100644 src/app/customers/[id]/edit/page.tsx create mode 100644 src/app/customers/[id]/page.tsx create mode 100644 src/app/customers/new/page.tsx create mode 100644 src/app/customers/page.tsx create mode 100644 src/app/releases/[id]/edit/page.tsx create mode 100644 src/app/releases/[id]/page.tsx create mode 100644 src/app/releases/[id]/steps/page.tsx create mode 100644 src/app/releases/new/page.tsx create mode 100644 src/app/releases/page.tsx create mode 100644 src/components/clusters/cluster-card.tsx create mode 100644 src/components/clusters/cluster-form.tsx create mode 100644 src/components/customers/customer-card.tsx create mode 100644 src/components/customers/customer-form.tsx create mode 100644 src/components/layout/sidebar.tsx create mode 100644 src/components/releases/release-card.tsx create mode 100644 src/components/releases/release-form.tsx create mode 100644 src/components/ui/alert-dialog.tsx create mode 100644 src/components/ui/alert.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/collapsible.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/scroll-area.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/components/ui/table.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/lib/actions/clusters.ts create mode 100644 src/lib/actions/customer-steps.ts create mode 100644 src/lib/actions/customers.ts create mode 100644 src/lib/actions/releases.ts create mode 100644 src/lib/actions/step-templates.ts create mode 100644 src/lib/db/index.ts create mode 100644 src/lib/db/schema.ts create mode 100644 src/lib/utils.ts diff --git a/.gitignore b/.gitignore index 5ef6a52..b970695 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,9 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# sqlite database +/data/*.db +/data/*.db-journal +/data/*.db-wal +/data/*.db-shm diff --git a/README.md b/README.md index e215bc4..41a974a 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,98 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Release Orchestration & Tracking System + +A web application to manage, track, and execute multi-customer deployment workflows across multiple Kubernetes clusters. + +## Features + +- **Multi-Cluster Support**: Manage multiple K8s clusters, each hosting one or more customers +- **Customer Management**: Organize customers by cluster with namespace isolation +- **Release Management**: Create and track releases of different types (onboarding, regular release, hotfix) +- **Step Templates**: Define common deployment and verification steps +- **Customer-Specific Customization**: Override or add custom steps per customer +- **Matrix View**: Visual progress tracking across all customers and clusters +- **Execution Tracking**: Mark steps as done, skipped, or reverted with notes + +## Tech Stack + +- **Framework**: Next.js 14+ (App Router) +- **Language**: TypeScript +- **Database**: SQLite (via better-sqlite3) +- **ORM**: Drizzle ORM +- **Styling**: Tailwind CSS +- **UI Components**: shadcn/ui ## Getting Started -First, run the development server: +### Prerequisites +- Node.js 20.x or higher +- npm or yarn + +### Installation + +1. Install dependencies: ```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +npm install ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +2. Run the development server: +```bash +npm run dev +``` -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +3. Open [http://localhost:3000](http://localhost:3000) in your browser. -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +### Database -## Learn More +The application uses SQLite with a local database file stored in `data/app.db`. The database schema is automatically initialized on first run. -To learn more about Next.js, take a look at the following resources: +## Usage -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +### 1. Setup Your Infrastructure -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +1. **Create Clusters**: Go to Clusters page and add your Kubernetes clusters +2. **Add Customers**: For each cluster, add customers with their namespaces -## Deploy on Vercel +### 2. Create a Release -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +1. Go to Releases page and click "Create Release" +2. Choose release type: + - **Onboarding**: For new customer setup + - **Regular Release**: Standard deployment with version number + - **Hotfix**: Emergency fixes +3. Define deployment and verification steps +4. Activate the release to generate customer steps -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +### 3. Track Progress + +1. View the matrix visualization to see all customers' progress +2. Click on individual steps to mark them as done or skipped +3. Override step content for specific customers if needed + +## Project Structure + +``` +my-app/ +├── src/ +│ ├── app/ # Next.js pages +│ ├── components/ # React components +│ ├── lib/ +│ │ ├── actions/ # Server Actions +│ │ ├── db/ # Database schema and connection +│ │ └── utils/ # Utilities +├── data/ # SQLite database +└── docs/ # Documentation (FSD, TSD) +``` + +## Future Enhancements + +- Auto-execution of bash/SQL scripts via K8s agents +- Multi-user support with authentication +- Audit logging +- Jenkins/Rancher API integrations +- Email notifications +- Scheduled releases + +## License + +MIT diff --git a/components.json b/components.json new file mode 100644 index 0000000..03909d9 --- /dev/null +++ b/components.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/FSD.md b/docs/FSD.md new file mode 100644 index 0000000..9c90580 --- /dev/null +++ b/docs/FSD.md @@ -0,0 +1,552 @@ +# Functional Specification Document +## Release Orchestration & Tracking System + +### Version: 1.0 +### Date: 2026-02-01 + +--- + +## 1. Overview + +### 1.1 Purpose +A web application to manage, track, and execute multi-customer deployment workflows across multiple Kubernetes clusters with customer-specific variations. + +### 1.2 Target User +Developer/Release manager managing multiple isolated customer environments across one or more K8s clusters. + +### 1.3 Core Value +- Eliminate forgotten steps across customers and clusters +- Prevent wrong script execution on wrong customers +- Provide clear visibility of deployment progress per customer and cluster +- Manage customers organized by their hosting clusters + +--- + +## 2. User Personas & Stories + +### Persona: Release Manager (Developer) + +| ID | User Story | +|----|-----------| +| US-001 | As a release manager, I want to create a release with deployment and verification steps so that I can standardize the release process | +| US-002 | As a release manager, I want to define common steps that apply to all customers so that I don't repeat myself | +| US-003 | As a release manager, I want to customize steps per customer so that customer-specific requirements are handled | +| US-004 | As a release manager, I want to track which steps are completed for each customer so that I don't miss anything | +| US-005 | As a release manager, I want to see the execution status at a glance so that I can identify blockers quickly | +| US-006 | As a release manager, I want to manage multiple K8s clusters so that I can organize customers by their hosting infrastructure | +| US-007 | As a release manager, I want to see which cluster a customer belongs to so that I can execute cluster-specific operations correctly | +| US-008 | As a release manager, I want to filter/view customers by cluster so that I can focus on one cluster at a time | + +--- + +## 3. Core Entities + +### 3.1 Entity Relationship Diagram + +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Cluster │────<│ Customer │>────│ CustomerStep │ +├─────────────────┤ 1:M ├──────────────────┤ M:1 ├─────────────────┤ +│ id │ │ id │ │ id │ +│ name │ │ cluster_id (FK) │ │ release_id (FK) │ +│ kubeconfig_path │ │ namespace │ │ customer_id(FK) │ +│ description │ │ name │ │ template_id(FK) │ +│ is_active │ │ description │ │ ... │ +│ metadata │ │ is_active │ │ status │ +│ created_at │ │ metadata │ │ content │ +└─────────────────┘ │ created_at │ └─────────────────┘ + └──────────────────┘ │ + ^ │ + │ │ + │ M:1 │ M:1 + │ │ + ┌──────────────────┐ ┌─────────────────┐ + │ StepTemplate │<────│ Release │ + ├──────────────────┤ M:1 ├─────────────────┤ + │ id │ │ id │ + │ release_id (FK) │ │ name │ + │ name │ │ type │ + │ type │ │ status │ + │ content │ │ version_number │ + │ category │ │ release_date │ + │ order_index │ │ description │ + └──────────────────┘ └─────────────────┘ +``` + +### 3.2 Entity Definitions + +#### Cluster +Represents a Kubernetes cluster that hosts one or more customers. + +| Field | Type | Description | +|-------|------|-------------| +| id | integer | Primary key | +| name | string | Cluster name (e.g., "production-eu", "staging-us") | +| kubeconfig_path | string | Path to kubeconfig file (optional, for future auto-execution) | +| description | text | Human-readable description | +| is_active | boolean | Whether cluster is active | +| metadata | json | Extensible metadata (API endpoints, region, etc.) | +| created_at | timestamp | Creation time | +| updated_at | timestamp | Last update time | + +#### Customer +Represents a tenant/customer deployed in a specific namespace within a cluster. + +| Field | Type | Description | +|-------|------|-------------| +| id | integer | Primary key | +| cluster_id | integer | FK to Cluster | +| namespace | string | K8s namespace (e.g., "customer-acme-prod") | +| name | string | Customer display name | +| description | text | Description | +| is_active | boolean | Whether customer is active | +| metadata | json | Extensible metadata | +| created_at | timestamp | Creation time | +| updated_at | timestamp | Last update time | + +#### Release +Represents a deployment/release cycle. + +| Field | Type | Description | +|-------|------|-------------| +| id | integer | Primary key | +| name | string | Release name | +| type | enum | `onboarding`, `release`, `hotfix` | +| status | enum | `draft`, `active`, `archived` | +| version_number | string | Version (e.g., "v2.5.0") for release type | +| release_date | timestamp | Target release date | +| description | text | Release notes/description | +| metadata | json | Additional properties | +| created_at | timestamp | Creation time | +| updated_at | timestamp | Last update time | + +#### StepTemplate +Common step definition at release level. + +| Field | Type | Description | +|-------|------|-------------| +| id | integer | Primary key | +| release_id | integer | FK to Release | +| name | string | Step name | +| category | enum | `deploy` or `verify` | +| type | enum | `bash`, `sql`, `text` | +| content | text | Step content (script, SQL, instructions) | +| order_index | integer | Execution order within category | +| description | text | Step description | +| created_at | timestamp | Creation time | + +#### CustomerStep +Customer-specific step instance (actual execution unit). + +| Field | Type | Description | +|-------|------|-------------| +| id | integer | Primary key | +| release_id | integer | FK to Release | +| customer_id | integer | FK to Customer | +| template_id | integer | FK to StepTemplate (nullable for custom steps) | +| name | string | Step name (copied or custom) | +| category | enum | `deploy` or `verify` | +| type | enum | `bash`, `sql`, `text` | +| content | text | Step content (may be overridden) | +| order_index | integer | Execution order | +| status | enum | `pending`, `done`, `skipped`, `reverted` | +| executed_at | timestamp | When step was executed | +| executed_by | string | Who executed (for future multi-user) | +| skip_reason | text | Reason if skipped | +| notes | text | Execution notes | +| is_custom | boolean | True if not from template | +| is_overridden | boolean | True if template content was changed | +| created_at | timestamp | Creation time | +| updated_at | timestamp | Last update time | + +--- + +## 4. Feature Specifications + +### 4.1 Cluster Management + +| ID | Feature | Description | +|----|---------|-------------| +| FC-001 | Create Cluster | Add new K8s cluster with name, kubeconfig path (optional), description | +| FC-002 | Edit Cluster | Update cluster details | +| FC-003 | Delete Cluster | Soft delete cluster (only if no active customers) | +| FC-004 | List Clusters | View all clusters with customer count | +| FC-005 | View Cluster Detail | See cluster info and list of customers in it | +| FC-006 | Cluster Filter | Filter customers/releases by cluster | + +### 4.2 Customer Management + +| ID | Feature | Description | +|----|---------|-------------| +| FM-001 | Create Customer | Add customer with name, namespace, and assign to cluster | +| FM-002 | Edit Customer | Update customer details or move to different cluster | +| FM-003 | Delete Customer | Soft delete (archive) customer | +| FM-004 | List Customers | View all customers with cluster and namespace info | +| FM-005 | View Customer Detail | See customer info, cluster, namespace, and release history | +| FM-006 | Filter by Cluster | Show only customers in selected cluster(s) | + +### 4.3 Release Management + +| ID | Feature | Description | +|----|---------|-------------| +| FR-001 | Create Release | Create with type, name, description, version/date for release type | +| FR-002 | Release Lifecycle | Draft → Active → Archived workflow | +| FR-003 | Activate Release | Copies template steps to all active customers | +| FR-004 | Clone Release | Use existing release as template for new one | +| FR-005 | Archive Release | Archive completed/abandoned releases | +| FR-006 | View Release Dashboard | See progress across all customers and clusters | +| FR-007 | Filter Dashboard | Filter by cluster to focus on specific infrastructure | + +### 4.4 Step Management (Template Layer) + +| ID | Feature | Description | +|----|---------|-------------| +| FS-001 | Add Template Step | Add common step with category, type, content | +| FS-002 | Edit Template Step | Modify common step (affects pending customer steps only) | +| FS-003 | Reorder Steps | Drag-and-drop to reorder steps | +| FS-004 | Delete Template Step | Remove step with warning | +| FS-005 | Syntax Highlight | Display bash/SQL/text with appropriate highlighting | + +### 4.5 Customer Step Customization + +| ID | Feature | Description | +|----|---------|-------------| +| FCS-001 | View Inherited Steps | See template steps applied to customer | +| FCS-002 | Override Step Content | Modify step content for specific customer | +| FCS-003 | Add Custom Step | Add step only for specific customer | +| FCS-004 | Skip Step | Mark step as skipped with mandatory reason | +| FCS-005 | Revert Override | Reset to template content | +| FCS-006 | Remove Custom Step | Delete customer-specific step | + +### 4.6 Execution Tracking + +| ID | Feature | Description | +|----|---------|-------------| +| FET-001 | Mark Done | Mark step as completed with optional notes | +| FET-002 | Mark Reverted | Revert a completed step | +| FET-003 | Skip Step | Skip with mandatory reason | +| FET-004 | Bulk Mark Done | Mark all deploy/verify steps done for customer | +| FET-005 | Progress Indicators | Show % complete per customer, per cluster, per category | + +### 4.7 Dashboard & Views + +| ID | Feature | Description | +|----|---------|-------------| +| FV-001 | Main Dashboard | Overview of active releases, recent activity | +| FV-002 | Release List | All releases with type badges and status | +| FV-003 | Release Matrix View | Grid: Steps × Customers with cluster grouping | +| FV-004 | Customer View | Steps list for specific customer | +| FV-005 | Step Cross-Customer View | Status of one step across all customers | +| FV-006 | Cluster View | All customers in a cluster with their release status | +| FV-007 | Step Detail Modal | Full content display with syntax highlight and copy | + +--- + +## 5. UI/UX Design + +### 5.1 Navigation Structure + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 🏠 Dashboard 🗂️ Clusters 👥 Customers 📦 Releases │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 5.2 Dashboard View + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Dashboard [+ Release] │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Active Releases │ │ Clusters │ │ Quick Stats │ │ +│ │ ━━━━━━━━━━━━━ │ │ ━━━━━━━━━━━━━ │ │ ━━━━━━━━━━━━━ │ │ +│ │ • v2.5.0 (Rel) │ │ • prod-us (3) │ │ Pending: 47 │ │ +│ │ • Hotfix-2024-1 │ │ • prod-eu (2) │ │ Done: 128 │ │ +│ │ │ │ • staging (1) │ │ Skipped: 5 │ │ +│ │ │ │ │ │ │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ +│ Recent Activity │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ • [v2.5.0] Customer A (prod-us) - Deploy Step 3 Done │ │ +│ │ • [v2.5.0] Customer B (prod-eu) - Verify Step 1 Skipped │ │ +│ │ ... │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 5.3 Clusters List View + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Clusters [+ Cluster] │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ 🟢 prod-us [Edit][🗑️] │ │ +│ │ Path: ~/.kube/prod-us-config │ │ +│ │ Region: us-east-1 | 3 Customers │ │ +│ │ Customers: customer-a, customer-b, customer-c │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ 🟢 prod-eu [Edit][🗑️] │ │ +│ │ Path: ~/.kube/prod-eu-config │ │ +│ │ Region: eu-west-1 | 2 Customers │ │ +│ │ Customers: customer-d, customer-e │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ 🟡 staging [Edit][🗑️] │ │ +│ │ Path: ~/.kube/staging-config │ │ +│ │ Region: internal | 1 Customer │ │ +│ │ Customers: demo-customer │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 5.4 Customers List View + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Customers [Filter: All Clusters ▼] [+ Add]│ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ 📁 prod-us (3 customers) │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ customer-a namespace: cust-a-prod [Edit] │ │ +│ │ customer-b namespace: cust-b-prod [Edit] │ │ +│ │ customer-c namespace: cust-c-prod [Edit] │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ 📁 prod-eu (2 customers) │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ customer-d namespace: cust-d-prod [Edit] │ │ +│ │ customer-e namespace: cust-e-prod [Edit] │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 5.5 Release Matrix View (Cluster-Aware) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Release: v2.5.0 (Regular Release) │ +│ Type: release | Status: Active | Date: 2024-01-15 │ +│ [Deploy Tab] [Verify Tab] [Settings] │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Filter: [All Clusters ▼] [All Status ▼] [Expand All] │ +│ │ +│ ───────────────────────────────────────────────────────────── │ +│ 📁 prod-us │ +│ ───────────────────────────────────────────────────────────── │ +│ ┌──────────┬─────────────────┬─────────────────┬────────────┐ │ +│ │ Step │ customer-a │ customer-b │ customer-c │ │ +│ │ │ (cust-a-prod) │ (cust-b-prod) │ (cust-c..) │ │ +│ ├──────────┼─────────────────┼─────────────────┼────────────┤ │ +│ │ 1. Deploy│ ✅ Done │ ✅ Done │ 🔄 Pending │ │ +│ │ 2. SQL ⭐ │ ✅ Done │ ⚠️ Overridden │ 🔄 Pending │ │ +│ │ 3. Config│ ⏸️ Skipped │ 🔄 Pending │ 🔄 Pending │ │ +│ └──────────┴─────────────────┴─────────────────┴────────────┘ │ +│ │ +│ ───────────────────────────────────────────────────────────── │ +│ 📁 prod-eu │ +│ ───────────────────────────────────────────────────────────── │ +│ ┌──────────┬─────────────────┬─────────────────┐ │ +│ │ Step │ customer-d │ customer-e │ │ +│ │ │ (cust-d-prod) │ (cust-e-prod) │ │ +│ ├──────────┼─────────────────┼─────────────────┤ │ +│ │ 1. Deploy│ ✅ Done │ 🔄 Pending │ │ +│ │ 2. SQL │ 🔄 Pending │ 🔄 Pending │ │ +│ │ 3. Config│ 🔄 Pending │ ⏸️ Skipped │ │ +│ └──────────┴─────────────────┴─────────────────┘ │ +│ │ +│ Legend: ✅ Done | 🔄 Pending | ⏸️ Skipped | ⚠️ Custom/Overridden │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 5.6 Customer Step Detail View + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Customer: customer-a │ +│ Cluster: prod-us | Namespace: cust-a-prod │ +│ Release: v2.5.0 [Back] │ +├─────────────────────────────────────────────────────────────────┤ +│ Deploy Steps (2/3 done) [Mark All] │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ ✅ 1. Deploy via Jenkins (Done) │ │ +│ │ Completed: 2024-01-15 10:30 │ │ +│ │ [View Content] [Revert] │ │ +│ ├─────────────────────────────────────────────────────────┤ │ +│ │ ✅ 2. Run Migration SQL ⭐ CUSTOM (Done) │ │ +│ │ Overridden from template │ │ +│ │ Completed: 2024-01-15 10:45 │ │ +│ │ [View Content] [Revert] [Reset to Template] │ │ +│ ├─────────────────────────────────────────────────────────┤ │ +│ │ 🔄 3. Update ConfigMap (Pending)│ │ +│ │ [View Content] [Mark Done] [Skip] [Edit] │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ Verify Steps (0/2 done) [Mark All] │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ 🔄 1. Health Check API (Pending)│ │ +│ │ [View Content] [Mark Done] [Skip] │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 5.7 Step Content Modal + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Step: Run Migration SQL [X] │ +├─────────────────────────────────────────────────────────────────┤ +│ Category: Deploy | Type: SQL │ +│ Source: Template (edited for this customer) │ +│ Customer: customer-a (prod-us/cust-a-prod) │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ ```sql │ │ +│ │ -- Custom migration for customer-a │ │ +│ │ ALTER TABLE users ADD COLUMN custom_field VARCHAR(100); │ │ +│ │ UPDATE users SET custom_field = 'value' WHERE id > 100; │ │ +│ │ ``` │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ [📋 Copy] [⬇️ Download] │ +│ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ Execution Notes: │ │ +│ │ [Optional: Add notes about this execution... ] │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ [Cancel] [Mark as Done] │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 6. Business Rules + +### 6.1 Cluster Rules +- **BR-C01**: Cluster cannot be deleted if it has active customers +- **BR-C02**: Cluster name must be unique +- **BR-C03**: When viewing releases, customers are grouped by cluster for clarity + +### 6.2 Customer Rules +- **BR-M01**: Customer namespace + cluster combination must be unique +- **BR-M02**: Customer can be moved between clusters (with warning about context change) +- **BR-M03**: Soft delete only; maintain history for audit + +### 6.3 Release Rules +- **BR-R01**: When release is activated, steps are created for ALL active customers across ALL clusters +- **BR-R02**: Editing template step only affects customers where step is `pending` +- **BR-R03**: Once step is marked `done`, content is locked (prevent accidental changes) +- **BR-R04**: Reverting a step sets status to `reverted` but preserves history + +### 6.4 Step Rules +- **BR-S01**: Custom steps (is_custom=true) don't affect other customers or template +- **BR-S02**: Override (is_overridden=true) preserves link to template for reference +- **BR-S03**: Skipping requires mandatory reason +- **BR-S04**: Order index is per category (deploy/verify have separate ordering) + +--- + +## 7. Workflow Scenarios + +### 7.1 New Customer Onboarding + +``` +1. Create new Cluster (if new infrastructure) + └─→ Enter name, kubeconfig path, description + +2. Create new Customer in Cluster + └─→ Select cluster + └─→ Enter namespace, name, description + +3. Create "onboarding" type Release + └─→ Enter release name, type=onboarding + └─→ Add template steps (common for all customers) + +4. Activate Release + └─→ System creates CustomerStep for new customer + └─→ Customize steps if needed for this customer + +5. Execute steps and track progress +``` + +### 7.2 Regular Release Deployment + +``` +1. Create "release" type Release + └─→ Enter version number (e.g., v2.5.0) + └─→ Set target release date + └─→ Add deploy steps and verify steps + +2. Activate Release + └─→ Steps copied to all customers across all clusters + +3. Execute per cluster or per customer + └─→ Use cluster filter to focus on one cluster + └─→ Mark steps done as completed + └─→ Override content if customer-specific changes needed + └─→ Add custom steps if needed + +4. Monitor progress via matrix view + └─→ Check all customers have completed all steps + +5. Archive release when done +``` + +### 7.3 Hotfix Deployment + +``` +1. Create "hotfix" type Release + └─→ Enter hotfix name/description + └─→ May target specific customers only (via custom steps) + +2. Add minimal steps for the fix + +3. Activate and deploy + +4. Verify and archive +``` + +--- + +## 8. Data Display Requirements + +### 8.1 Cluster Information Display +Everywhere a customer is shown, the following should be visible: +- Cluster name +- Namespace + +### 8.2 Progress Calculation +- **Per Customer**: (done_steps / total_steps) × 100 +- **Per Cluster**: Average of all customers in cluster +- **Overall**: Average of all customers across all clusters +- **By Category**: Separate progress for deploy vs verify + +### 8.3 Filtering & Sorting +- Filter customers by cluster +- Filter matrix view by cluster +- Sort customers by name, cluster, or progress + +--- + +## 9. Future Extensibility + +| Feature | Description | Current Preparation | +|---------|-------------|---------------------| +| Auto-execution | Execute bash/SQL via K8s agents | kubeconfig_path in Cluster, step type field | +| Multi-user | Authentication & authorization | executed_by field in CustomerStep | +| Audit log | Full change history | Template references, timestamps, override tracking | +| Batch operations | Execute on multiple clusters | Cluster grouping in UI | +| Integration APIs | Jenkins webhook, Rancher API | Metadata JSON fields for API endpoints | +| Scheduling | Schedule releases for specific time | release_date field | +| Notifications | Alert on step completion/failure | Status tracking infrastructure | diff --git a/docs/TSD.md b/docs/TSD.md new file mode 100644 index 0000000..4170331 --- /dev/null +++ b/docs/TSD.md @@ -0,0 +1,1163 @@ +# Technical Specification Document +## Release Orchestration & Tracking System + +### Version: 1.0 +### Date: 2026-02-01 + +--- + +## 1. Technology Stack + +| Layer | Technology | Version | Rationale | +|-------|-----------|---------|-----------| +| **Framework** | Next.js | 14+ (App Router) | Full-stack React, API routes, RSC support | +| **Language** | TypeScript | 5.x | Type safety, better DX | +| **Runtime** | Node.js | 20.x LTS | Stable, good performance | +| **Database** | SQLite | 3.x | Zero-config, single-file, sufficient for single-user | +| **ORM** | Drizzle ORM | Latest | Type-safe SQL, lightweight, migrations support | +| **Styling** | Tailwind CSS | 3.x | Utility-first, rapid UI development | +| **UI Components** | shadcn/ui | Latest | Accessible, customizable components | +| **Icons** | Lucide React | Latest | Clean, consistent icons | +| **Syntax Highlight** | PrismJS | Latest | Code display for bash/SQL | +| **State Management** | React Server Components + Server Actions | Built-in | Simplified data flow, minimal client JS | + +### Alternative Considerations + +| Alternative | Why Not Chosen | Migration Path | +|-------------|----------------|----------------| +| PostgreSQL | Overkill for single-user tool | Can migrate if multi-user needed later | +| Prisma | Heavier, requires client generation | Drizzle is lighter and faster | +| tRPC | Server Actions sufficient | Can add if API complexity grows | +| Redux/Zustand | Server-state preferred | Already using RSC pattern | + +--- + +## 2. Database Schema + +### 2.1 Drizzle ORM Schema Definition + +```typescript +// lib/db/schema.ts + +import { sqliteTable, integer, text, uniqueIndex } from 'drizzle-orm/sqlite-core'; +import { relations } from 'drizzle-orm'; + +// ==================== Clusters ==================== +export const clusters = sqliteTable('clusters', { + id: integer('id').primaryKey({ autoIncrement: true }), + name: text('name').notNull().unique(), + kubeconfigPath: text('kubeconfig_path'), + description: text('description'), + isActive: integer('is_active', { mode: 'boolean' }).default(true), + metadata: text('metadata', { mode: 'json' }).$type>(), + createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), + updatedAt: integer('updated_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), +}); + +export const clustersRelations = relations(clusters, ({ many }) => ({ + customers: many(customers), +})); + +// ==================== Customers ==================== +export const customers = sqliteTable('customers', { + id: integer('id').primaryKey({ autoIncrement: true }), + clusterId: integer('cluster_id').notNull().references(() => clusters.id, { onDelete: 'restrict' }), + namespace: text('namespace').notNull(), + name: text('name').notNull(), + description: text('description'), + isActive: integer('is_active', { mode: 'boolean' }).default(true), + metadata: text('metadata', { mode: 'json' }).$type>(), + createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), + updatedAt: integer('updated_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), +}, (table) => ({ + uniqueNamespacePerCluster: uniqueIndex('unique_namespace_per_cluster') + .on(table.clusterId, table.namespace), +})); + +export const customersRelations = relations(customers, ({ one, many }) => ({ + cluster: one(clusters, { fields: [customers.clusterId], references: [clusters.id] }), + steps: many(customerSteps), +})); + +// ==================== Releases ==================== +export const releases = sqliteTable('releases', { + id: integer('id').primaryKey({ autoIncrement: true }), + name: text('name').notNull(), + type: text('type', { enum: ['onboarding', 'release', 'hotfix'] }).notNull(), + status: text('status', { enum: ['draft', 'active', 'archived'] }).default('draft'), + versionNumber: text('version_number'), + releaseDate: integer('release_date', { mode: 'timestamp' }), + description: text('description'), + metadata: text('metadata', { mode: 'json' }).$type>(), + createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), + updatedAt: integer('updated_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), +}); + +export const releasesRelations = relations(releases, ({ many }) => ({ + templates: many(stepTemplates), + customerSteps: many(customerSteps), +})); + +// ==================== Step Templates ==================== +export const stepTemplates = sqliteTable('step_templates', { + id: integer('id').primaryKey({ autoIncrement: true }), + releaseId: integer('release_id').notNull().references(() => releases.id, { onDelete: 'cascade' }), + name: text('name').notNull(), + category: text('category', { enum: ['deploy', 'verify'] }).notNull(), + type: text('type', { enum: ['bash', 'sql', 'text'] }).notNull(), + content: text('content').notNull(), + orderIndex: integer('order_index').notNull(), + description: text('description'), + createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), +}, (table) => ({ + uniqueOrderPerReleaseCategory: uniqueIndex('unique_order_per_release_category') + .on(table.releaseId, table.category, table.orderIndex), +})); + +export const stepTemplatesRelations = relations(stepTemplates, ({ one, many }) => ({ + release: one(releases, { fields: [stepTemplates.releaseId], references: [releases.id] }), + customerSteps: many(customerSteps), +})); + +// ==================== Customer Steps ==================== +export const customerSteps = sqliteTable('customer_steps', { + id: integer('id').primaryKey({ autoIncrement: true }), + releaseId: integer('release_id').notNull().references(() => releases.id, { onDelete: 'cascade' }), + customerId: integer('customer_id').notNull().references(() => customers.id, { onDelete: 'cascade' }), + templateId: integer('template_id').references(() => stepTemplates.id, { onDelete: 'set null' }), + + // Copied/Overridden fields + name: text('name').notNull(), + category: text('category', { enum: ['deploy', 'verify'] }).notNull(), + type: text('type', { enum: ['bash', 'sql', 'text'] }).notNull(), + content: text('content').notNull(), + orderIndex: integer('order_index').notNull(), + + // Execution tracking + status: text('status', { enum: ['pending', 'done', 'skipped', 'reverted'] }).default('pending'), + executedAt: integer('executed_at', { mode: 'timestamp' }), + executedBy: text('executed_by'), + skipReason: text('skip_reason'), + notes: text('notes'), + + // Flags + isCustom: integer('is_custom', { mode: 'boolean' }).default(false), + isOverridden: integer('is_overridden', { mode: 'boolean' }).default(false), + + createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), + updatedAt: integer('updated_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), +}, (table) => ({ + // Ensure unique steps per customer per release + uniqueCustomerStep: uniqueIndex('unique_customer_step') + .on(table.releaseId, table.customerId, table.templateId), +})); + +export const customerStepsRelations = relations(customerSteps, ({ one }) => ({ + release: one(releases, { fields: [customerSteps.releaseId], references: [releases.id] }), + customer: one(customers, { fields: [customerSteps.customerId], references: [customers.id] }), + template: one(stepTemplates, { fields: [customerSteps.templateId], references: [stepTemplates.id] }), +})); + +// ==================== Types ==================== +export type Cluster = typeof clusters.$inferSelect; +export type NewCluster = typeof clusters.$inferInsert; + +export type Customer = typeof customers.$inferSelect; +export type NewCustomer = typeof customers.$inferInsert; + +export type Release = typeof releases.$inferSelect; +export type NewRelease = typeof releases.$inferInsert; + +export type StepTemplate = typeof stepTemplates.$inferSelect; +export type NewStepTemplate = typeof stepTemplates.$inferInsert; + +export type CustomerStep = typeof customerSteps.$inferSelect; +export type NewCustomerStep = typeof customerSteps.$inferInsert; + +export type StepCategory = 'deploy' | 'verify'; +export type StepType = 'bash' | 'sql' | 'text'; +export type ReleaseType = 'onboarding' | 'release' | 'hotfix'; +export type ReleaseStatus = 'draft' | 'active' | 'archived'; +export type StepStatus = 'pending' | 'done' | 'skipped' | 'reverted'; +``` + +### 2.2 Database Indexes + +| Index | Purpose | +|-------|---------| +| `unique_namespace_per_cluster` | Prevent duplicate namespaces within a cluster | +| `unique_order_per_release_category` | Prevent duplicate ordering within a release category | +| `unique_customer_step` | Prevent duplicate steps per customer per release | +| `customer_steps.release_id` | Foreign key index for release lookups | +| `customer_steps.customer_id` | Foreign key index for customer lookups | +| `step_templates.release_id` | Foreign key index for template lookups | +| `customers.cluster_id` | Foreign key index for cluster lookups | + +--- + +## 3. Project Structure + +``` +my-app/ +├── app/ # Next.js App Router +│ ├── page.tsx # Dashboard (main entry) +│ ├── layout.tsx # Root layout +│ ├── globals.css # Global styles +│ │ +│ ├── clusters/ # Cluster management +│ │ ├── page.tsx # List all clusters +│ │ ├── new/ +│ │ │ └── page.tsx # Create cluster form +│ │ └── [id]/ +│ │ ├── page.tsx # Cluster detail +│ │ └── edit/ +│ │ └── page.tsx # Edit cluster form +│ │ +│ ├── customers/ # Customer management +│ │ ├── page.tsx # List all customers (grouped by cluster) +│ │ ├── new/ +│ │ │ └── page.tsx # Create customer form +│ │ └── [id]/ +│ │ ├── page.tsx # Customer detail +│ │ └── edit/ +│ │ └── page.tsx # Edit customer form +│ │ +│ └── releases/ # Release management +│ ├── page.tsx # List all releases +│ ├── new/ +│ │ └── page.tsx # Create release wizard +│ └── [id]/ +│ ├── page.tsx # Release dashboard (matrix view) +│ ├── steps/ +│ │ └── page.tsx # Manage template steps +│ └── customer/ +│ └── [customerId]/ +│ └── page.tsx # Customer-specific steps view +│ +├── components/ # React components +│ ├── ui/ # shadcn/ui components (auto-generated) +│ │ ├── button.tsx +│ │ ├── card.tsx +│ │ ├── dialog.tsx +│ │ ├── input.tsx +│ │ ├── select.tsx +│ │ ├── table.tsx +│ │ ├── tabs.tsx +│ │ ├── badge.tsx +│ │ └── ... +│ │ +│ ├── clusters/ +│ │ ├── cluster-card.tsx +│ │ ├── cluster-form.tsx +│ │ └── cluster-list.tsx +│ │ +│ ├── customers/ +│ │ ├── customer-card.tsx +│ │ ├── customer-form.tsx +│ │ ├── customer-list.tsx +│ │ └── customer-select.tsx +│ │ +│ ├── releases/ +│ │ ├── release-card.tsx +│ │ ├── release-form.tsx +│ │ ├── release-matrix.tsx # Main matrix view +│ │ ├── release-progress.tsx +│ │ └── release-type-badge.tsx +│ │ +│ ├── steps/ +│ │ ├── step-card.tsx +│ │ ├── step-editor.tsx # Code editor with syntax highlight +│ │ ├── step-form.tsx +│ │ ├── step-list.tsx +│ │ ├── step-detail-modal.tsx +│ │ ├── step-status-badge.tsx +│ │ └── step-type-icon.tsx +│ │ +│ └── layout/ +│ ├── sidebar.tsx +│ ├── header.tsx +│ └── breadcrumb.tsx +│ +├── lib/ # Utilities and shared code +│ ├── db/ +│ │ ├── index.ts # Database connection +│ │ ├── schema.ts # Drizzle schema +│ │ └── migrations/ # Migration files +│ │ +│ ├── actions/ # Server Actions +│ │ ├── clusters.ts +│ │ ├── customers.ts +│ │ ├── releases.ts +│ │ ├── step-templates.ts +│ │ └── customer-steps.ts +│ │ +│ ├── utils/ +│ │ ├── cn.ts # Tailwind merge utility +│ │ ├── formatting.ts # Date/text formatting +│ │ └── validations.ts # Input validations +│ │ +│ └── types/ +│ └── index.ts # Shared TypeScript types +│ +├── public/ # Static assets +│ +├── drizzle.config.ts # Drizzle configuration +├── next.config.js # Next.js configuration +├── tailwind.config.ts # Tailwind configuration +├── tsconfig.json # TypeScript configuration +└── package.json +``` + +--- + +## 4. Server Actions API + +### 4.1 Cluster Actions + +```typescript +// lib/actions/clusters.ts +'use server'; + +import { db } from '@/lib/db'; +import { clusters, type NewCluster, type Cluster } from '@/lib/db/schema'; +import { eq, and } from 'drizzle-orm'; +import { revalidatePath } from 'next/cache'; + +export async function createCluster(data: NewCluster): Promise { + const [cluster] = await db.insert(clusters).values(data).returning(); + revalidatePath('/clusters'); + return cluster; +} + +export async function updateCluster( + id: number, + data: Partial +): Promise { + const [cluster] = await db + .update(clusters) + .set({ ...data, updatedAt: new Date() }) + .where(eq(clusters.id, id)) + .returning(); + revalidatePath('/clusters'); + revalidatePath(`/clusters/${id}`); + return cluster; +} + +export async function deleteCluster(id: number): Promise { + // Check if cluster has active customers + const customerCount = await db.query.customers.count({ + where: and( + eq(customers.clusterId, id), + eq(customers.isActive, true) + ) + }); + + if (customerCount > 0) { + throw new Error('Cannot delete cluster with active customers'); + } + + await db.update(clusters) + .set({ isActive: false, updatedAt: new Date() }) + .where(eq(clusters.id, id)); + revalidatePath('/clusters'); +} + +export async function listClusters(): Promise { + return db.query.clusters.findMany({ + where: eq(clusters.isActive, true), + orderBy: clusters.name, + }); +} + +export async function getClusterWithCustomers(id: number) { + return db.query.clusters.findFirst({ + where: eq(clusters.id, id), + with: { + customers: { + where: eq(customers.isActive, true), + orderBy: customers.name, + }, + }, + }); +} +``` + +### 4.2 Customer Actions + +```typescript +// lib/actions/customers.ts +'use server'; + +import { db } from '@/lib/db'; +import { customers, type NewCustomer, type Customer } from '@/lib/db/schema'; +import { eq, and } from 'drizzle-orm'; +import { revalidatePath } from 'next/cache'; + +export async function createCustomer(data: NewCustomer): Promise { + const [customer] = await db.insert(customers).values(data).returning(); + revalidatePath('/customers'); + revalidatePath(`/clusters/${data.clusterId}`); + return customer; +} + +export async function updateCustomer( + id: number, + data: Partial +): Promise { + const [customer] = await db + .update(customers) + .set({ ...data, updatedAt: new Date() }) + .where(eq(customers.id, id)) + .returning(); + revalidatePath('/customers'); + revalidatePath(`/customers/${id}`); + return customer; +} + +export async function deleteCustomer(id: number): Promise { + await db.update(customers) + .set({ isActive: false, updatedAt: new Date() }) + .where(eq(customers.id, id)); + revalidatePath('/customers'); +} + +export async function listCustomers(): Promise { + return db.query.customers.findMany({ + where: eq(customers.isActive, true), + with: { cluster: true }, + orderBy: customers.name, + }); +} + +export async function listCustomersByCluster(clusterId: number): Promise { + return db.query.customers.findMany({ + where: and( + eq(customers.clusterId, clusterId), + eq(customers.isActive, true) + ), + orderBy: customers.name, + }); +} + +export async function getCustomerWithCluster(id: number) { + return db.query.customers.findFirst({ + where: eq(customers.id, id), + with: { cluster: true }, + }); +} +``` + +### 4.3 Release Actions + +```typescript +// lib/actions/releases.ts +'use server'; + +import { db } from '@/lib/db'; +import { + releases, + stepTemplates, + customerSteps, + customers, + type NewRelease, + type Release +} from '@/lib/db/schema'; +import { eq, and, inArray } from 'drizzle-orm'; +import { revalidatePath } from 'next/cache'; + +export async function createRelease(data: NewRelease): Promise { + const [release] = await db.insert(releases).values(data).returning(); + revalidatePath('/releases'); + return release; +} + +export async function updateRelease( + id: number, + data: Partial +): Promise { + const [release] = await db + .update(releases) + .set({ ...data, updatedAt: new Date() }) + .where(eq(releases.id, id)) + .returning(); + revalidatePath('/releases'); + revalidatePath(`/releases/${id}`); + return release; +} + +export async function activateRelease(id: number): Promise { + const release = await db.query.releases.findFirst({ + where: eq(releases.id, id), + with: { templates: true }, + }); + + if (!release) throw new Error('Release not found'); + if (release.status !== 'draft') throw new Error('Release is not in draft status'); + + const activeCustomers = await db.query.customers.findMany({ + where: eq(customers.isActive, true), + }); + + // Create customer steps from templates + const customerStepsToInsert = activeCustomers.flatMap(customer => + release.templates.map(template => ({ + releaseId: id, + customerId: customer.id, + templateId: template.id, + name: template.name, + category: template.category, + type: template.type, + content: template.content, + orderIndex: template.orderIndex, + status: 'pending' as const, + isCustom: false, + isOverridden: false, + })) + ); + + if (customerStepsToInsert.length > 0) { + await db.insert(customerSteps).values(customerStepsToInsert); + } + + await db.update(releases) + .set({ status: 'active', updatedAt: new Date() }) + .where(eq(releases.id, id)); + + revalidatePath('/releases'); + revalidatePath(`/releases/${id}`); +} + +export async function archiveRelease(id: number): Promise { + await db.update(releases) + .set({ status: 'archived', updatedAt: new Date() }) + .where(eq(releases.id, id)); + revalidatePath('/releases'); +} + +export async function listReleases(): Promise { + return db.query.releases.findMany({ + orderBy: (releases, { desc }) => [desc(releases.createdAt)], + }); +} + +export async function getReleaseWithProgress(id: number) { + const release = await db.query.releases.findFirst({ + where: eq(releases.id, id), + with: { + templates: { + orderBy: [stepTemplates.category, stepTemplates.orderIndex], + }, + }, + }); + + if (!release) return null; + + // Get customer steps with customer and cluster info + const steps = await db.query.customerSteps.findMany({ + where: eq(customerSteps.releaseId, id), + with: { + customer: { + with: { cluster: true }, + }, + }, + }); + + // Group by cluster for display + const groupedByCluster = steps.reduce((acc, step) => { + const clusterName = step.customer.cluster.name; + if (!acc[clusterName]) acc[clusterName] = []; + acc[clusterName].push(step); + return acc; + }, {} as Record); + + return { ...release, groupedByCluster, allSteps: steps }; +} +``` + +### 4.4 Step Template Actions + +```typescript +// lib/actions/step-templates.ts +'use server'; + +import { db } from '@/lib/db'; +import { stepTemplates, customerSteps, type NewStepTemplate, type StepTemplate } from '@/lib/db/schema'; +import { eq, and } from 'drizzle-orm'; +import { revalidatePath } from 'next/cache'; + +export async function addTemplateStep(data: NewStepTemplate): Promise { + const [template] = await db.insert(stepTemplates).values(data).returning(); + revalidatePath(`/releases/${data.releaseId}/steps`); + return template; +} + +export async function updateTemplateStep( + id: number, + data: Partial +): Promise { + const [template] = await db + .update(stepTemplates) + .set(data) + .where(eq(stepTemplates.id, id)) + .returning(); + + // Update pending customer steps + if (data.content) { + await db.update(customerSteps) + .set({ content: data.content }) + .where(and( + eq(customerSteps.templateId, id), + eq(customerSteps.status, 'pending'), + eq(customerSteps.isOverridden, false) + )); + } + + revalidatePath(`/releases/${template.releaseId}/steps`); + return template; +} + +export async function deleteTemplateStep(id: number): Promise { + const template = await db.query.stepTemplates.findFirst({ + where: eq(stepTemplates.id, id), + }); + + if (!template) return; + + // Delete associated customer steps that haven't been executed + await db.delete(customerSteps) + .where(and( + eq(customerSteps.templateId, id), + eq(customerSteps.status, 'pending') + )); + + await db.delete(stepTemplates).where(eq(stepTemplates.id, id)); + revalidatePath(`/releases/${template.releaseId}/steps`); +} + +export async function reorderSteps( + releaseId: number, + category: 'deploy' | 'verify', + orderedIds: number[] +): Promise { + for (let i = 0; i < orderedIds.length; i++) { + await db.update(stepTemplates) + .set({ orderIndex: i }) + .where(eq(stepTemplates.id, orderedIds[i])); + + // Also update customer steps + await db.update(customerSteps) + .set({ orderIndex: i }) + .where(and( + eq(customerSteps.templateId, orderedIds[i]), + eq(customerSteps.category, category) + )); + } + revalidatePath(`/releases/${releaseId}/steps`); +} +``` + +### 4.5 Customer Step Actions + +```typescript +// lib/actions/customer-steps.ts +'use server'; + +import { db } from '@/lib/db'; +import { customerSteps, type CustomerStep } from '@/lib/db/schema'; +import { eq } from 'drizzle-orm'; +import { revalidatePath } from 'next/cache'; + +export async function getCustomerSteps( + releaseId: number, + customerId: number +): Promise { + return db.query.customerSteps.findMany({ + where: and( + eq(customerSteps.releaseId, releaseId), + eq(customerSteps.customerId, customerId) + ), + orderBy: [customerSteps.category, customerSteps.orderIndex], + }); +} + +export async function overrideStepContent( + stepId: number, + newContent: string +): Promise { + const [step] = await db + .update(customerSteps) + .set({ + content: newContent, + isOverridden: true, + updatedAt: new Date() + }) + .where(eq(customerSteps.id, stepId)) + .returning(); + revalidatePath(`/releases/${step.releaseId}`); + return step; +} + +export async function addCustomStep( + releaseId: number, + customerId: number, + data: { + name: string; + category: 'deploy' | 'verify'; + type: 'bash' | 'sql' | 'text'; + content: string; + orderIndex: number; + } +): Promise { + const [step] = await db.insert(customerSteps).values({ + ...data, + releaseId, + customerId, + templateId: null, + status: 'pending', + isCustom: true, + isOverridden: false, + }).returning(); + revalidatePath(`/releases/${releaseId}`); + return step; +} + +export async function markStepDone( + stepId: number, + notes?: string +): Promise { + const [step] = await db + .update(customerSteps) + .set({ + status: 'done', + executedAt: new Date(), + notes: notes || null, + updatedAt: new Date() + }) + .where(eq(customerSteps.id, stepId)) + .returning(); + revalidatePath(`/releases/${step.releaseId}`); + return step; +} + +export async function markStepReverted( + stepId: number, + reason?: string +): Promise { + const [step] = await db + .update(customerSteps) + .set({ + status: 'reverted', + notes: reason || null, + updatedAt: new Date() + }) + .where(eq(customerSteps.id, stepId)) + .returning(); + revalidatePath(`/releases/${step.releaseId}`); + return step; +} + +export async function skipStep( + stepId: number, + reason: string +): Promise { + const [step] = await db + .update(customerSteps) + .set({ + status: 'skipped', + skipReason: reason, + updatedAt: new Date() + }) + .where(eq(customerSteps.id, stepId)) + .returning(); + revalidatePath(`/releases/${step.releaseId}`); + return step; +} + +export async function bulkMarkDone(stepIds: number[]): Promise { + for (const id of stepIds) { + await markStepDone(id); + } +} + +export async function resetToTemplate(stepId: number): Promise { + const step = await db.query.customerSteps.findFirst({ + where: eq(customerSteps.id, stepId), + with: { template: true }, + }); + + if (!step?.template) throw new Error('No template found'); + + const [updated] = await db + .update(customerSteps) + .set({ + content: step.template.content, + name: step.template.name, + isOverridden: false, + updatedAt: new Date() + }) + .where(eq(customerSteps.id, stepId)) + .returning(); + revalidatePath(`/releases/${step.releaseId}`); + return updated; +} +``` + +--- + +## 5. Key Implementation Details + +### 5.1 Release Activation Algorithm + +```typescript +async function activateRelease(releaseId: number) { + // 1. Validate release is in draft status + // 2. Fetch all templates for the release + // 3. Fetch all active customers with their clusters + // 4. For each customer, create CustomerStep for each template + // 5. Copy template content to customer step (denormalization) + // 6. Set status = 'pending' for all + // 7. Update release status to 'active' +} +``` + +### 5.2 Matrix View Data Query + +```typescript +// Optimized query for the matrix view +async function getReleaseMatrix(releaseId: number, clusterId?: number) { + const whereClause = eq(customerSteps.releaseId, releaseId); + + const steps = await db.query.customerSteps.findMany({ + where: clusterId + ? and(whereClause, eq(customers.clusterId, clusterId)) + : whereClause, + with: { + customer: { + with: { cluster: true }, + }, + template: true, + }, + orderBy: [customerSteps.category, customerSteps.orderIndex], + }); + + // Transform into matrix format + // Rows: Steps (grouped by category) + // Columns: Customers (grouped by cluster) + const matrix = { + deploy: { steps: [], customers: {} }, + verify: { steps: [], customers: {} }, + }; + + // Grouping logic... + return matrix; +} +``` + +### 5.3 Progress Calculation + +```typescript +function calculateProgress(steps: CustomerStep[]) { + const total = steps.length; + const done = steps.filter(s => s.status === 'done').length; + const skipped = steps.filter(s => s.status === 'skipped').length; + const pending = steps.filter(s => s.status === 'pending').length; + const reverted = steps.filter(s => s.status === 'reverted').length; + + return { + total, + done, + skipped, + pending, + reverted, + percentage: Math.round(((done + skipped) / total) * 100), + }; +} +``` + +### 5.4 Syntax Highlighting + +```typescript +// components/steps/code-block.tsx +import Prism from 'prismjs'; +import 'prismjs/components/prism-sql'; +import 'prismjs/components/prism-bash'; + +interface CodeBlockProps { + code: string; + type: 'bash' | 'sql' | 'text'; +} + +export function CodeBlock({ code, type }: CodeBlockProps) { + const language = type === 'text' ? 'text' : type; + const highlighted = Prism.highlight( + code, + Prism.languages[language] || Prism.languages.text, + language + ); + + return ( +
+      
+    
+ ); +} +``` + +--- + +## 6. Component Specifications + +### 6.1 Release Matrix Component + +```typescript +// components/releases/release-matrix.tsx + +interface ReleaseMatrixProps { + releaseId: number; + clusterId?: number; // Optional filter +} + +interface MatrixData { + clusters: { + id: number; + name: string; + customers: { + id: number; + name: string; + namespace: string; + steps: CustomerStep[]; + }[]; + }[]; + deploySteps: StepTemplate[]; + verifySteps: StepTemplate[]; +} + +export function ReleaseMatrix({ releaseId, clusterId }: ReleaseMatrixProps) { + // Fetches data and renders cluster-grouped matrix + // Each cluster is a collapsible section + // Table: Steps as rows, Customers as columns + // Cells: Status badge with quick actions +} +``` + +### 6.2 Step Card Component + +```typescript +// components/steps/step-card.tsx + +interface StepCardProps { + step: CustomerStep; + onMarkDone: (id: number) => void; + onMarkReverted: (id: number) => void; + onSkip: (id: number, reason: string) => void; + onEdit: (id: number, content: string) => void; +} + +export function StepCard({ step, ...actions }: StepCardProps) { + // Displays step name, type badge, status badge + // Shows cluster/namespace context + // Actions based on current status + // Modal for viewing full content +} +``` + +--- + +## 7. Configuration + +### 7.1 Environment Variables + +```bash +# .env.local +# Database +DATABASE_URL="file:./data/app.db" + +# Optional: For future integrations +JENKINS_URL="" +JENKINS_TOKEN="" +RANCHER_URL="" +RANCHER_TOKEN="" +``` + +### 7.2 Drizzle Configuration + +```typescript +// drizzle.config.ts +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './lib/db/schema.ts', + out: './lib/db/migrations', + driver: 'better-sqlite3', + dbCredentials: { + url: process.env.DATABASE_URL || 'file:./data/app.db', + }, +}); +``` + +### 7.3 Next.js Configuration + +```javascript +// next.config.js +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + serverActions: true, + }, + // Ensure SQLite database is not bundled + webpack: (config, { isServer }) => { + if (!isServer) { + config.resolve.fallback = { + ...config.resolve.fallback, + fs: false, + path: false, + }; + } + return config; + }, +}; + +module.exports = nextConfig; +``` + +--- + +## 8. Database Migration Strategy + +### Initial Migration + +```bash +# 1. Generate migration +npx drizzle-kit generate:sqlite + +# 2. Apply migration +npx drizzle-kit push:sqlite +``` + +### Schema Updates + +```bash +# After modifying schema.ts: +npx drizzle-kit generate:sqlite +npx drizzle-kit push:sqlite +``` + +--- + +## 9. Development Workflow + +### 9.1 Setup + +```bash +# 1. Initialize project +npx create-next-app@latest my-app --typescript --tailwind --app + +# 2. Install dependencies +cd my-app +npm install drizzle-orm better-sqlite3 +npm install -D drizzle-kit @types/better-sqlite3 + +# 3. Initialize shadcn/ui +npx shadcn-ui@latest init + +# 4. Add components +npx shadcn-ui@latest add button card dialog input select table tabs badge + +# 5. Setup database +mkdir -p data +touch data/.gitkeep +``` + +### 9.2 Development Server + +```bash +npm run dev +# Runs on http://localhost:3000 +``` + +### 9.3 Build + +```bash +npm run build +npm start +``` + +--- + +## 10. Security Considerations + +| Concern | Mitigation | +|---------|-----------| +| SQL Injection | Drizzle ORM parameterized queries | +| XSS | React's built-in escaping, sanitize user input | +| CSRF | Next.js Server Actions built-in CSRF protection | +| Path Traversal | Validate file paths for kubeconfig | +| Data Integrity | Foreign key constraints, transactions | + +--- + +## 11. Performance Considerations + +| Area | Strategy | +|------|----------| +| Database | Proper indexes on foreign keys and common queries | +| Data Fetching | React Server Components for initial data | +| Re-rendering | Server Actions with revalidatePath | +| Large Lists | Virtualization if customer count grows large | +| Bundle Size | Tree-shaking, dynamic imports for heavy components | + +--- + +## 12. Testing Strategy + +| Type | Approach | +|------|----------| +| Unit | Test utilities, validations | +| Integration | Test Server Actions with test database | +| E2E | Playwright for critical user flows | + +--- + +## 13. Deployment + +### 13.1 Production Build + +```bash +npm run build +``` + +### 13.2 Data Persistence + +- SQLite database stored in `data/app.db` +- Mount data directory as volume in container +- Backup strategy: Regular file backups + +### 13.3 Docker (Optional) + +```dockerfile +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build +VOLUME ["/app/data"] +EXPOSE 3000 +CMD ["npm", "start"] +``` + +--- + +## 14. Future Extensibility + +| Feature | Technical Preparation | +|---------|----------------------| +| Multi-user | Add `users` table, auth middleware, row-level security | +| Auto-execution | Background job queue, K8s client libraries | +| Webhooks | API routes for Jenkins/Rancher callbacks | +| Notifications | WebSocket or Server-Sent Events for real-time updates | +| Audit log | Separate `audit_logs` table with triggers | +| Export/Import | CSV/JSON export endpoints | diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..4c69cda --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/lib/db/schema.ts', + out: './src/lib/db/migrations', + dialect: 'sqlite', + dbCredentials: { + url: process.env.DATABASE_URL || 'file:./data/app.db', + }, +}); diff --git a/next.config.ts b/next.config.ts index e9ffa30..8c52d7f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,13 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + experimental: { + serverActions: { + bodySizeLimit: '2mb', + }, + }, + // Disable turbopack for now since we need webpack config for better-sqlite3 + turbopack: {}, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 977a229..6f02383 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,18 +8,39 @@ "name": "my-app", "version": "0.1.0", "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "better-sqlite3": "^12.6.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "drizzle-orm": "^0.45.1", + "lucide-react": "^0.563.0", "next": "16.1.6", + "prismjs": "^1.30.0", "react": "19.2.3", - "react-dom": "19.2.3" + "react-dom": "19.2.3", + "tailwind-merge": "^3.4.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/better-sqlite3": "^7.6.13", "@types/node": "^20", + "@types/prismjs": "^1.26.5", "@types/react": "^19", "@types/react-dom": "^19", + "drizzle-kit": "^0.31.8", "eslint": "^9", "eslint-config-next": "16.1.6", "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", "typescript": "^5" } }, @@ -277,6 +298,13 @@ "node": ">=6.9.0" } }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -310,6 +338,884 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "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/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -454,6 +1360,44 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", + "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.5" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1227,6 +2171,981 @@ "node": ">=12.4.0" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1525,6 +3444,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1550,17 +3480,24 @@ "version": "20.19.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -1571,8 +3508,9 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2180,6 +4118,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -2410,6 +4360,26 @@ "dev": true, "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.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", @@ -2419,6 +4389,41 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/better-sqlite3": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz", + "integrity": "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2478,6 +4483,37 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-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==", + "dev": true, + "license": "MIT" + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2575,12 +4611,39 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2634,7 +4697,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2716,6 +4779,30 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2763,12 +4850,17 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2782,6 +4874,147 @@ "node": ">=0.10.0" } }, + "node_modules/drizzle-kit": { + "version": "0.31.8", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.8.tgz", + "integrity": "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "esbuild-register": "^3.5.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.1.tgz", + "integrity": "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2811,6 +5044,15 @@ "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", @@ -3002,6 +5244,62 @@ "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", + "peer": true, + "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/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3217,6 +5515,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3450,6 +5749,15 @@ "node": ">=0.10.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3524,6 +5832,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3591,6 +5905,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3677,6 +5997,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3722,6 +6051,12 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3896,6 +6231,26 @@ "hermes-estree": "0.25.1" } }, + "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/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3933,6 +6288,18 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4839,6 +7206,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", + "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4883,6 +7259,18 @@ "node": ">=8.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4900,12 +7288,17 @@ "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/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4931,6 +7324,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -5035,6 +7434,30 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -5165,6 +7588,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5331,6 +7763,32 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5341,6 +7799,15 @@ "node": ">= 0.8.0" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5353,6 +7820,16 @@ "react-is": "^16.13.1" } }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5384,6 +7861,30 @@ ], "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", @@ -5414,6 +7915,89 @@ "dev": true, "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5554,6 +8138,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -5811,6 +8415,61 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map": { + "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/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5820,6 +8479,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -5841,6 +8511,15 @@ "node": ">= 0.4" } }, + "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.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6026,6 +8705,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", @@ -6047,6 +8736,34 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -6154,6 +8871,28 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6307,7 +9046,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -6386,6 +9125,55 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6501,6 +9289,12 @@ "node": ">=0.10.0" } }, + "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/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 0858890..771cf04 100644 --- a/package.json +++ b/package.json @@ -9,18 +9,39 @@ "lint": "eslint" }, "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "better-sqlite3": "^12.6.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "drizzle-orm": "^0.45.1", + "lucide-react": "^0.563.0", "next": "16.1.6", + "prismjs": "^1.30.0", "react": "19.2.3", - "react-dom": "19.2.3" + "react-dom": "19.2.3", + "tailwind-merge": "^3.4.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/better-sqlite3": "^7.6.13", "@types/node": "^20", + "@types/prismjs": "^1.26.5", "@types/react": "^19", "@types/react-dom": "^19", + "drizzle-kit": "^0.31.8", "eslint": "^9", "eslint-config-next": "16.1.6", "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", "typescript": "^5" } } diff --git a/src/app/api/releases/[id]/route.ts b/src/app/api/releases/[id]/route.ts new file mode 100644 index 0000000..d1d4743 --- /dev/null +++ b/src/app/api/releases/[id]/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getReleaseById } from '@/lib/actions/releases'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const release = await getReleaseById(parseInt(id)); + + if (!release) { + return NextResponse.json({ error: 'Release not found' }, { status: 404 }); + } + + return NextResponse.json(release); + } catch (error) { + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/steps/[id]/route.ts b/src/app/api/steps/[id]/route.ts new file mode 100644 index 0000000..b262e58 --- /dev/null +++ b/src/app/api/steps/[id]/route.ts @@ -0,0 +1,18 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { deleteStepTemplate } from '@/lib/actions/step-templates'; + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + await deleteStepTemplate(parseInt(id)); + return NextResponse.json({ success: true }); + } catch (error) { + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/steps/route.ts b/src/app/api/steps/route.ts new file mode 100644 index 0000000..9969154 --- /dev/null +++ b/src/app/api/steps/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { addStepTemplate, getNextOrderIndex } from '@/lib/actions/step-templates'; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { releaseId, category, name, type, content, description } = body; + + const orderIndex = await getNextOrderIndex(releaseId, category); + + const step = await addStepTemplate({ + releaseId, + category, + name, + type, + content, + orderIndex, + description, + }); + + return NextResponse.json(step); + } catch (error) { + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/src/app/clusters/[id]/edit/page.tsx b/src/app/clusters/[id]/edit/page.tsx new file mode 100644 index 0000000..8b9e19f --- /dev/null +++ b/src/app/clusters/[id]/edit/page.tsx @@ -0,0 +1,30 @@ +import { notFound } from 'next/navigation'; +import { ClusterForm } from '@/components/clusters/cluster-form'; +import { getClusterById } from '@/lib/actions/clusters'; + +interface EditClusterPageProps { + params: Promise<{ + id: string; + }>; +} + +export default async function EditClusterPage({ params }: EditClusterPageProps) { + const { id } = await params; + const cluster = await getClusterById(parseInt(id)); + + if (!cluster) { + notFound(); + } + + return ( +
+
+

Edit Cluster

+

+ Update cluster information +

+
+ +
+ ); +} diff --git a/src/app/clusters/[id]/page.tsx b/src/app/clusters/[id]/page.tsx new file mode 100644 index 0000000..6bc8bf8 --- /dev/null +++ b/src/app/clusters/[id]/page.tsx @@ -0,0 +1,101 @@ +import Link from 'next/link'; +import { notFound } from 'next/navigation'; +import { ArrowLeft, Server, Users } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { getClusterWithCustomers } from '@/lib/actions/clusters'; + +interface ClusterDetailPageProps { + params: Promise<{ + id: string; + }>; +} + +export default async function ClusterDetailPage({ params }: ClusterDetailPageProps) { + const { id } = await params; + const cluster = await getClusterWithCustomers(parseInt(id)); + + if (!cluster) { + notFound(); + } + + return ( +
+
+ + + +
+

{cluster.name}

+

Cluster Details

+
+
+ +
+ + + + + Cluster Information + + + + {cluster.kubeconfigPath && ( +
+ +

{cluster.kubeconfigPath}

+
+ )} + {cluster.description && ( +
+ +

{cluster.description}

+
+ )} +
+ +

{cluster.createdAt ? new Date(cluster.createdAt).toLocaleString() : 'N/A'}

+
+
+
+ + + + + + Customers ({cluster.customers.length}) + + + + {cluster.customers.length === 0 ? ( +

No customers in this cluster yet.

+ ) : ( +
    + {cluster.customers.map((customer) => ( +
  • + +

    {customer.name}

    +

    {customer.namespace}

    + +
  • + ))} +
+ )} +
+ + + +
+
+
+
+
+ ); +} diff --git a/src/app/clusters/new/page.tsx b/src/app/clusters/new/page.tsx new file mode 100644 index 0000000..2a099e6 --- /dev/null +++ b/src/app/clusters/new/page.tsx @@ -0,0 +1,15 @@ +import { ClusterForm } from '@/components/clusters/cluster-form'; + +export default function NewClusterPage() { + return ( +
+
+

Create Cluster

+

+ Add a new Kubernetes cluster to manage your customers +

+
+ +
+ ); +} diff --git a/src/app/clusters/page.tsx b/src/app/clusters/page.tsx new file mode 100644 index 0000000..52625a5 --- /dev/null +++ b/src/app/clusters/page.tsx @@ -0,0 +1,60 @@ +import Link from 'next/link'; +import { Plus } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { ClusterCard } from '@/components/clusters/cluster-card'; +import { listClusters } from '@/lib/actions/clusters'; +import { db } from '@/lib/db'; +import { customers } from '@/lib/db/schema'; +import { eq, count } from 'drizzle-orm'; + +export default async function ClustersPage() { + const clusters = await listClusters(); + + // Get customer counts for each cluster + const clustersWithCount = await Promise.all( + clusters.map(async (cluster) => { + const result = await db + .select({ value: count() }) + .from(customers) + .where(eq(customers.clusterId, cluster.id)); + return { + ...cluster, + customerCount: result[0]?.value || 0, + }; + }) + ); + + return ( +
+
+
+

Clusters

+

+ Manage your Kubernetes clusters +

+
+ + + +
+ + {clustersWithCount.length === 0 ? ( +
+

No clusters yet. Create your first cluster to get started.

+ + + +
+ ) : ( +
+ {clustersWithCount.map((cluster) => ( + + ))} +
+ )} +
+ ); +} diff --git a/src/app/customers/[id]/edit/page.tsx b/src/app/customers/[id]/edit/page.tsx new file mode 100644 index 0000000..b25dc34 --- /dev/null +++ b/src/app/customers/[id]/edit/page.tsx @@ -0,0 +1,34 @@ +import { notFound } from 'next/navigation'; +import { CustomerForm } from '@/components/customers/customer-form'; +import { getCustomerById } from '@/lib/actions/customers'; +import { listClusters } from '@/lib/actions/clusters'; + +interface EditCustomerPageProps { + params: Promise<{ + id: string; + }>; +} + +export default async function EditCustomerPage({ params }: EditCustomerPageProps) { + const { id } = await params; + const [customer, clusters] = await Promise.all([ + getCustomerById(parseInt(id)), + listClusters(), + ]); + + if (!customer) { + notFound(); + } + + return ( +
+
+

Edit Customer

+

+ Update customer information +

+
+ +
+ ); +} diff --git a/src/app/customers/[id]/page.tsx b/src/app/customers/[id]/page.tsx new file mode 100644 index 0000000..a638de8 --- /dev/null +++ b/src/app/customers/[id]/page.tsx @@ -0,0 +1,136 @@ +import Link from 'next/link'; +import { notFound } from 'next/navigation'; +import { ArrowLeft, Users, Server, Package } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { getCustomerById } from '@/lib/actions/customers'; +import { listReleases } from '@/lib/actions/releases'; +import { getCustomerSteps } from '@/lib/actions/customer-steps'; + +interface CustomerDetailPageProps { + params: Promise<{ + id: string; + }>; +} + +export default async function CustomerDetailPage({ params }: CustomerDetailPageProps) { + const { id } = await params; + const customer = await getCustomerById(parseInt(id)); + + if (!customer) { + notFound(); + } + + const releases = await listReleases(); + + // Get steps for each active release + const releasesWithSteps = await Promise.all( + releases + .filter(r => r.status === 'active') + .map(async (release) => { + const steps = await getCustomerSteps(release.id, customer.id); + const done = steps.filter(s => s.status === 'done').length; + const total = steps.length; + return { + ...release, + steps, + progress: total > 0 ? Math.round((done / total) * 100) : 0, + done, + total, + }; + }) + ); + + return ( +
+
+ + + +
+

{customer.name}

+

+ + {customer.cluster?.name} / {customer.namespace} +

+
+
+ +
+ + + + + Customer Information + + + +
+ +

{customer.namespace}

+
+
+ +

{customer.cluster?.name}

+
+ {customer.description && ( +
+ +

{customer.description}

+
+ )} +
+ +

{customer.createdAt ? new Date(customer.createdAt).toLocaleString() : 'N/A'}

+
+
+
+ +
+ + + + + Active Releases + + + + {releasesWithSteps.length === 0 ? ( +

No active releases.

+ ) : ( +
+ {releasesWithSteps.map((release) => ( +
+
+
+

{release.name}

+ + {release.type} + +
+

+ {release.done} / {release.total} steps completed +

+
+
+
+ {release.progress}% +
+ + + +
+
+ ))} +
+ )} +
+
+
+
+
+ ); +} diff --git a/src/app/customers/new/page.tsx b/src/app/customers/new/page.tsx new file mode 100644 index 0000000..8ba1e6f --- /dev/null +++ b/src/app/customers/new/page.tsx @@ -0,0 +1,34 @@ +import { notFound } from 'next/navigation'; +import { CustomerForm } from '@/components/customers/customer-form'; +import { listClusters } from '@/lib/actions/clusters'; + +export default async function NewCustomerPage() { + const clusters = await listClusters(); + + if (clusters.length === 0) { + return ( +
+
+

Create Customer

+
+
+

+ You need to create a cluster first before adding customers. +

+
+
+ ); + } + + return ( +
+
+

Create Customer

+

+ Add a new customer to a cluster +

+
+ +
+ ); +} diff --git a/src/app/customers/page.tsx b/src/app/customers/page.tsx new file mode 100644 index 0000000..337aa22 --- /dev/null +++ b/src/app/customers/page.tsx @@ -0,0 +1,75 @@ +import Link from 'next/link'; +import { Plus } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { CustomerCard } from '@/components/customers/customer-card'; +import { listClusters } from '@/lib/actions/clusters'; +import { listCustomers } from '@/lib/actions/customers'; + +export default async function CustomersPage() { + const [customers, clusters] = await Promise.all([ + listCustomers(), + listClusters(), + ]); + + // Group customers by cluster + const groupedCustomers = customers.reduce((acc, customer) => { + const clusterId = customer.cluster?.id || 0; + const clusterName = customer.cluster?.name || 'Unknown Cluster'; + + if (!acc[clusterId]) { + acc[clusterId] = { + clusterName, + customers: [], + }; + } + acc[clusterId].customers.push(customer); + return acc; + }, {} as Record); + + return ( +
+
+
+

Customers

+

+ Manage your customers grouped by cluster +

+
+ + + +
+ + {customers.length === 0 ? ( +
+

No customers yet. Create your first customer to get started.

+ + + +
+ ) : ( +
+ {Object.entries(groupedCustomers).map(([clusterId, group]) => ( +
+

+ + {group.clusterName} + + ({group.customers.length} customers) + +

+
+ {group.customers.map((customer) => ( + + ))} +
+
+ ))} +
+ )} +
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..6cf72ed 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,125 @@ @import "tailwindcss"; +@import "tw-animate-css"; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; } } - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..157caa0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import { Sidebar } from "@/components/layout/sidebar"; +import { initDb } from "@/lib/db"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -13,10 +15,13 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Release Tracker", + description: "Release Orchestration & Tracking System", }; +// Initialize database on startup +initDb().catch(console.error); + export default function RootLayout({ children, }: Readonly<{ @@ -25,9 +30,14 @@ export default function RootLayout({ return ( - {children} +
+ +
+ {children} +
+
); diff --git a/src/app/page.tsx b/src/app/page.tsx index 295f8fd..c627a8a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,235 @@ -import Image from "next/image"; +import Link from 'next/link'; +import { Package, Server, Users, CheckCircle, Clock, SkipForward } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { listClusters } from '@/lib/actions/clusters'; +import { listCustomers } from '@/lib/actions/customers'; +import { getActiveReleases, getReleaseStats } from '@/lib/actions/releases'; +import { db } from '@/lib/db'; +import { customers } from '@/lib/db/schema'; +import { eq, count } from 'drizzle-orm'; + +export default async function DashboardPage() { + const [clusters, customersList, activeReleases, stats] = await Promise.all([ + listClusters(), + listCustomers(), + getActiveReleases(), + getReleaseStats(), + ]); + + // Get customer counts per cluster + const clustersWithCount = await Promise.all( + clusters.map(async (cluster) => { + const result = await db + .select({ value: count() }) + .from(customers) + .where(eq(customers.clusterId, cluster.id)); + return { + ...cluster, + customerCount: result[0]?.value || 0, + }; + }) + ); -export default function Home() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- -
+
+ {/* Header */} +
+

Dashboard

+

+ Overview of your release orchestration system +

+
+ + {/* Stats Cards */} +
+ + + + Active Releases + + + + +
{stats.activeReleases}
+

+ of {stats.totalReleases} total +

+
+
+ + + + + Pending Steps + + + + +
{stats.pendingSteps}
+

+ awaiting completion +

+
+
+ + + + + Completed Steps + + + + +
{stats.doneSteps}
+

+ successfully done +

+
+
+ + + + + Total Customers + + + + +
{customersList.length}
+

+ across {clusters.length} clusters +

+
+
+
+ +
+ {/* Active Releases */} + + + Active Releases + + + + + + {activeReleases.length === 0 ? ( +
+

No active releases.

+ + + +
+ ) : ( +
+ {activeReleases.slice(0, 5).map((release) => ( + +
+
+

{release.name}

+ + {release.type} + +
+ {release.versionNumber && ( +

{release.versionNumber}

+ )} +
+ + + ))} +
+ )} +
+
+ + {/* Clusters Overview */} + + + Clusters + + + + + + {clustersWithCount.length === 0 ? ( +
+

No clusters configured.

+ + + +
+ ) : ( +
+ {clustersWithCount.map((cluster) => ( + +
+ + {cluster.name} +
+ {cluster.customerCount} + + ))} +
+ )} +
+
+
+ + {/* Quick Actions */} +
+ + + +
+ +
+
+

Create Release

+

Start a new deployment cycle

+
+
+
+ + + + + +
+ +
+
+

Add Cluster

+

Register a new K8s cluster

+
+
+
+ + + + + +
+ +
+
+

Add Customer

+

Onboard a new customer

+
+
+
+ +
); } diff --git a/src/app/releases/[id]/edit/page.tsx b/src/app/releases/[id]/edit/page.tsx new file mode 100644 index 0000000..377c891 --- /dev/null +++ b/src/app/releases/[id]/edit/page.tsx @@ -0,0 +1,30 @@ +import { notFound } from 'next/navigation'; +import { ReleaseForm } from '@/components/releases/release-form'; +import { getReleaseById } from '@/lib/actions/releases'; + +interface EditReleasePageProps { + params: Promise<{ + id: string; + }>; +} + +export default async function EditReleasePage({ params }: EditReleasePageProps) { + const { id } = await params; + const release = await getReleaseById(parseInt(id)); + + if (!release) { + notFound(); + } + + return ( +
+
+

Edit Release

+

+ Update release information +

+
+ +
+ ); +} diff --git a/src/app/releases/[id]/page.tsx b/src/app/releases/[id]/page.tsx new file mode 100644 index 0000000..2bdab8f --- /dev/null +++ b/src/app/releases/[id]/page.tsx @@ -0,0 +1,363 @@ +import Link from 'next/link'; +import { notFound, redirect } from 'next/navigation'; +import { ArrowLeft, Edit, Play, Archive, Plus, CheckCircle, Circle, SkipForward, RotateCcw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Separator } from '@/components/ui/separator'; +import { getReleaseById, activateRelease, archiveRelease } from '@/lib/actions/releases'; +import { getReleaseStepsGroupedByCluster, getStepStats } from '@/lib/actions/customer-steps'; +import { listCustomers } from '@/lib/actions/customers'; + +interface ReleaseDetailPageProps { + params: Promise<{ + id: string; + }>; +} + +const typeColors: Record = { + onboarding: 'bg-purple-100 text-purple-800', + release: 'bg-blue-100 text-blue-800', + hotfix: 'bg-red-100 text-red-800', +}; + +const statusColors: Record = { + draft: 'bg-slate-100 text-slate-800', + active: 'bg-green-100 text-green-800', + archived: 'bg-gray-100 text-gray-800', +}; + +function getTypeColor(type: string) { + return typeColors[type] || 'bg-slate-100 text-slate-800'; +} + +function getStatusColor(status: string) { + return statusColors[status] || 'bg-slate-100 text-slate-800'; +} + +export default async function ReleaseDetailPage({ params }: ReleaseDetailPageProps) { + const { id } = await params; + const releaseId = parseInt(id); + const release = await getReleaseById(releaseId); + + if (!release) { + notFound(); + } + + // Get steps grouped by cluster + const stepsByCluster = release.status === 'active' + ? await getReleaseStepsGroupedByCluster(releaseId) + : {}; + + const stats = release.status === 'active' + ? await getStepStats(releaseId) + : { total: 0, done: 0, skipped: 0, pending: 0, reverted: 0, percentage: 0 }; + + const customers = await listCustomers(); + + async function handleActivate() { + 'use server'; + await activateRelease(releaseId); + redirect(`/releases/${releaseId}`); + } + + async function handleArchive() { + 'use server'; + await archiveRelease(releaseId); + redirect(`/releases/${releaseId}`); + } + + return ( +
+ {/* Header */} +
+
+ + + +
+
+

{release.name}

+ {release.type} + {release.status} +
+ {release.versionNumber && ( +

Version: {release.versionNumber}

+ )} +
+
+
+ + + + {release.status === 'draft' && ( +
+ +
+ )} + {release.status === 'active' && ( +
+ +
+ )} +
+
+ + {/* Release Info */} + + +
+
+ +

+ {release.releaseDate + ? new Date(release.releaseDate).toLocaleDateString() + : 'Not set'} +

+
+
+ +

{customers.length}

+
+ {release.status === 'active' && ( +
+ +
+
+
+
+ {stats.percentage}% +
+
+ )} +
+ {release.description && ( + <> + +
+ +

{release.description}

+
+ + )} + + + + {/* Steps Management (for draft) */} + {release.status === 'draft' && ( + + + Template Steps + + +
+
+

Deploy Steps ({release.templates.filter(t => t.category === 'deploy').length})

+ + + +
+
+

Verify Steps ({release.templates.filter(t => t.category === 'verify').length})

+ + + +
+
+
+
+ )} + + {/* Matrix View (for active) */} + {release.status === 'active' && ( + + + + Deploy ({stats.done + stats.skipped}/{stats.total}) + + + Verify + + + + + + + + + + + + )} +
+ ); +} + +interface MatrixViewProps { + stepsByCluster: any; + category: 'deploy' | 'verify'; + releaseId: number; +} + +function MatrixView({ stepsByCluster, category, releaseId }: MatrixViewProps) { + const clusters = Object.values(stepsByCluster); + + if (clusters.length === 0) { + return ( + + + No customers found. Add customers to see the matrix view. + + + ); + } + + return ( +
+ {clusters.map((clusterData: any) => { + const customers = Object.values(clusterData.customers); + + // Get all unique steps for this category + const allSteps = new Map(); + customers.forEach((customer: any) => { + customer.steps + .filter((s: any) => s.category === category) + .forEach((step: any) => { + if (!allSteps.has(step.name)) { + allSteps.set(step.name, step); + } + }); + }); + const steps = Array.from(allSteps.values()).sort((a: any, b: any) => a.orderIndex - b.orderIndex); + + if (steps.length === 0) return null; + + return ( + + + + + {clusterData.cluster?.name || 'Unknown Cluster'} + + + +
+ + + + + {customers.map((customer: any) => ( + + ))} + + + + {steps.map((step: any, stepIndex: number) => ( + + + {customers.map((customer: any) => { + const customerStep = customer.steps.find( + (s: any) => s.name === step.name && s.category === category + ); + + if (!customerStep) return ; + + return ( + + ); + })} + + ))} + +
Step +
{customer.customer.name}
+
{customer.customer.namespace}
+
+
+ {stepIndex + 1}. +
+

{step.name}

+ {step.isOverridden && ( + custom + )} +
+
+
+ +
+
+
+
+ ); + })} +
+ ); +} + +function StepStatusCell({ step, releaseId }: { step: any; releaseId: number }) { + const statusIcons = { + pending: , + done: , + skipped: , + reverted: , + }; + + async function markDone() { + 'use server'; + const { markStepDone } = await import('@/lib/actions/customer-steps'); + await markStepDone(step.id); + } + + async function skipStep(formData: FormData) { + 'use server'; + const { skipStep } = await import('@/lib/actions/customer-steps'); + await skipStep(step.id, formData.get('reason') as string); + } + + return ( +
+
+ +
+ {step.status === 'pending' && ( +
+ + +
+ )} +
+ ); +} diff --git a/src/app/releases/[id]/steps/page.tsx b/src/app/releases/[id]/steps/page.tsx new file mode 100644 index 0000000..f190910 --- /dev/null +++ b/src/app/releases/[id]/steps/page.tsx @@ -0,0 +1,235 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { notFound } from 'next/navigation'; +import { ArrowLeft, Plus, GripVertical, Trash2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Badge } from '@/components/ui/badge'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { useEffect } from 'react'; + +interface Step { + id: number; + name: string; + category: 'deploy' | 'verify'; + type: 'bash' | 'sql' | 'text'; + content: string; + orderIndex: number; + description?: string; +} + +interface PageProps { + params: Promise<{ + id: string; + }>; +} + +export default function StepsPage({ params }: PageProps) { + const [releaseId, setReleaseId] = useState(''); + const [release, setRelease] = useState(null); + const [deploySteps, setDeploySteps] = useState([]); + const [verifySteps, setVerifySteps] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + params.then(({ id }) => { + setReleaseId(id); + loadData(id); + }); + }, [params]); + + async function loadData(id: string) { + try { + const response = await fetch(`/api/releases/${id}`); + if (!response.ok) throw new Error('Failed to load'); + const data = await response.json(); + setRelease(data); + setDeploySteps(data.templates.filter((s: Step) => s.category === 'deploy').sort((a: Step, b: Step) => a.orderIndex - b.orderIndex)); + setVerifySteps(data.templates.filter((s: Step) => s.category === 'verify').sort((a: Step, b: Step) => a.orderIndex - b.orderIndex)); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + } + + if (isLoading) return
Loading...
; + if (!release) return notFound(); + + return ( +
+
+ + + +
+

Manage Steps

+

{release.name}

+
+
+ + + + + Deploy Steps ({deploySteps.length}) + + + Verify Steps ({verifySteps.length}) + + + + + loadData(releaseId)} + /> + + + + loadData(releaseId)} + /> + + +
+ ); +} + +interface StepListProps { + steps: Step[]; + category: 'deploy' | 'verify'; + releaseId: number; + onUpdate: () => void; +} + +function StepList({ steps, category, releaseId, onUpdate }: StepListProps) { + const [isDialogOpen, setIsDialogOpen] = useState(false); + + async function handleAddStep(formData: FormData) { + try { + const response = await fetch('/api/steps', { + method: 'POST', + body: JSON.stringify({ + releaseId, + category, + name: formData.get('name'), + type: formData.get('type'), + content: formData.get('content'), + description: formData.get('description'), + }), + }); + if (response.ok) { + setIsDialogOpen(false); + onUpdate(); + } + } catch (error) { + console.error(error); + } + } + + async function handleDeleteStep(stepId: number) { + if (!confirm('Are you sure you want to delete this step?')) return; + try { + const response = await fetch(`/api/steps/${stepId}`, { method: 'DELETE' }); + if (response.ok) onUpdate(); + } catch (error) { + console.error(error); + } + } + + return ( + + + {category} Steps + + + + + + + Add {category} Step + +
+
+ + +
+
+ + +
+
+ +