diff --git a/.github/workflows/AdminWebpage-Deploy-WF.yml b/.github/workflows/AdminWebpage-Deploy-WF.yml index 700b8e9..1e59f66 100644 --- a/.github/workflows/AdminWebpage-Deploy-WF.yml +++ b/.github/workflows/AdminWebpage-Deploy-WF.yml @@ -26,6 +26,13 @@ env: APP_SERVICE_NAME: WA-DeliveryBot-Admin-dev BOTNET_API_URL: https://ewu-deliverybotsystem-api.mangocoast-332176b0.westus2.azurecontainerapps.io SIMULATOR_API_URL: https://deliverybot-robot-simulator.mangocoast-332176b0.westus2.azurecontainerapps.io + ORDER_SERVICE_URL: https://deliverybot-order-service.mangocoast-332176b0.westus2.azurecontainerapps.io + # Entra ID staff sign-in (issue #54). Blank → auth disabled (app runs open). + # Fill these in from the app registration to switch sign-in on, then push. + # Client/tenant/group IDs are not secrets (a public SPA exposes them anyway). + ENTRA_CLIENT_ID: "b5a029c3-d046-4005-9497-23ba18df70b2" + ENTRA_TENANT_ID: "37321907-14a5-4390-987d-ec0c66c655cd" + ENTRA_ADMIN_GROUP_ID: "14fcd995-e89f-4020-b5ff-4a9b48a5824e" jobs: build-and-deploy: @@ -64,6 +71,10 @@ jobs: env: VITE_BOTNET_API_URL: ${{ env.BOTNET_API_URL }} VITE_SIMULATOR_API_URL: ${{ env.SIMULATOR_API_URL }} + VITE_ORDER_SERVICE_URL: ${{ env.ORDER_SERVICE_URL }} + VITE_ENTRA_CLIENT_ID: ${{ env.ENTRA_CLIENT_ID }} + VITE_ENTRA_TENANT_ID: ${{ env.ENTRA_TENANT_ID }} + VITE_ENTRA_ADMIN_GROUP_ID: ${{ env.ENTRA_ADMIN_GROUP_ID }} run: npm run build # ── 3. Deploy the build to the App Service ───────────────────────────── diff --git a/OrderService/OrderService/Controllers/OrdersController.cs b/OrderService/OrderService/Controllers/OrdersController.cs index b4a1df5..5d7b0e6 100644 --- a/OrderService/OrderService/Controllers/OrdersController.cs +++ b/OrderService/OrderService/Controllers/OrdersController.cs @@ -34,11 +34,14 @@ public async Task GetOrder(Guid id) return order is null ? NotFound() : Ok(order); } - // GET /api/orders?customerId=xxx — returns full order history for a customer + // GET /api/orders — returns all orders (admin view, issue #53) + // GET /api/orders?customerId=xxx — returns full order history for one customer [HttpGet] - public async Task GetOrderHistory([FromQuery] string customerId) + public async Task GetOrders([FromQuery] string? customerId) { - var orders = await _orderService.GetOrderHistoryAsync(customerId); + var orders = string.IsNullOrWhiteSpace(customerId) + ? await _orderService.GetAllOrdersAsync() + : await _orderService.GetOrderHistoryAsync(customerId); return Ok(orders); } } diff --git a/OrderService/OrderService/Program.cs b/OrderService/OrderService/Program.cs index ba9ce42..45fff43 100644 --- a/OrderService/OrderService/Program.cs +++ b/OrderService/OrderService/Program.cs @@ -32,6 +32,19 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +// ── CORS ───────────────────────────────────────────────────────────────────── +// Allow the Admin & Maintenance App (issue #18, Orders view #53) to call this +// API from the browser. +builder.Services.AddCors(options => +{ + options.AddPolicy("AdminApp", policy => + policy.WithOrigins( + "https://wa-deliverybot-admin-dev.azurewebsites.net", // deployed admin app + "http://localhost:5173") // local Vite dev server + .AllowAnyHeader() + .AllowAnyMethod()); +}); + var app = builder.Build(); // ── Auto-migrate on startup ──────────────────────────────────────────────────── @@ -56,6 +69,7 @@ app.UseSwaggerUI(); app.UseHttpsRedirection(); +app.UseCors("AdminApp"); app.MapControllers(); app.Run(); diff --git a/OrderService/OrderService/Services/IOrderService.cs b/OrderService/OrderService/Services/IOrderService.cs index 7a182da..0fbdcfa 100644 --- a/OrderService/OrderService/Services/IOrderService.cs +++ b/OrderService/OrderService/Services/IOrderService.cs @@ -9,6 +9,7 @@ public interface IOrderService Task PlaceOrderAsync(PlaceOrderDto dto); Task GetOrderAsync(Guid id); Task> GetOrderHistoryAsync(string customerId); + Task> GetAllOrdersAsync(); // Advances order status in response to a bot event from the simulator (#41). Task ApplyStatusEventAsync(RobotEventEnvelope evt, CancellationToken ct = default); diff --git a/OrderService/OrderService/Services/OrderService.cs b/OrderService/OrderService/Services/OrderService.cs index 076a137..96ae843 100644 --- a/OrderService/OrderService/Services/OrderService.cs +++ b/OrderService/OrderService/Services/OrderService.cs @@ -89,6 +89,17 @@ public async Task> GetOrderHistoryAsync(string cus return orders.Select(ToResponseDto); } + // Returns every order, newest first. Backs the admin Orders view (issue #53). + public async Task> GetAllOrdersAsync() + { + var orders = await _db.Orders + .Include(o => o.Items) + .OrderByDescending(o => o.CreatedAt) + .ToListAsync(); + + return orders.Select(ToResponseDto); + } + public async Task ApplyStatusEventAsync(RobotEventEnvelope evt, CancellationToken ct = default) { // Never react to events we published ourselves (RobotOrderAssignment). diff --git a/admin-webapp/.env.example b/admin-webapp/.env.example index 3f59bdf..b15d4ec 100644 --- a/admin-webapp/.env.example +++ b/admin-webapp/.env.example @@ -10,3 +10,14 @@ VITE_BOTNET_API_URL=https://ewu-deliverybotsystem-api.mangocoast-332176b0.westus # When unset, simulator sync is silently disabled and the admin app operates # against BotNet alone — useful before the simulator is deployed locally. VITE_SIMULATOR_API_URL=https://deliverybot-robot-simulator.mangocoast-332176b0.westus2.azurecontainerapps.io + +# Base URL of the Order Service (issue #22). Backs the Orders tab (issue #53). +# When unset, the Orders view falls back to mock data. +VITE_ORDER_SERVICE_URL=https://deliverybot-order-service.mangocoast-332176b0.westus2.azurecontainerapps.io + +# Entra ID staff sign-in (issue #54). Leave blank to run with auth disabled. +# Fill from the "DeliveryBot Admin App" registration to enable sign-in. +VITE_ENTRA_CLIENT_ID=b5a029c3-d046-4005-9497-23ba18df70b2 +VITE_ENTRA_TENANT_ID=37321907-14a5-4390-987d-ec0c66c655cd +# Object ID of the DeliveryBot-Admin security group (gates who can sign in). +VITE_ENTRA_ADMIN_GROUP_ID=14fcd995-e89f-4020-b5ff-4a9b48a5824e diff --git a/admin-webapp/package-lock.json b/admin-webapp/package-lock.json index 60412ad..20d64ce 100644 --- a/admin-webapp/package-lock.json +++ b/admin-webapp/package-lock.json @@ -8,6 +8,8 @@ "name": "admin-webapp", "version": "0.0.0", "dependencies": { + "@azure/msal-browser": "^5.11.0", + "@azure/msal-react": "^5.4.2", "react": "^19.2.6", "react-dom": "^19.2.6" }, @@ -55,6 +57,41 @@ "dev": true, "license": "ISC" }, + "node_modules/@azure/msal-browser": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.11.0.tgz", + "integrity": "sha512-zkGNYS3TwY8lUpPIafAmsFCYZbgFixY9y/LZB9GUg0IILoHTqpN26j5OrkL1AQThh/YdZsawe4iWXfp85lFVxg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@azure/msal-common": "16.6.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.6.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.2.tgz", + "integrity": "sha512-hQjjsekAjB00cM1EmatWJlzhEoK2Qhz7Rj5gvM6tYf8iL7RM3tkxlpU9fG0+ofkulzg9AEEA6dIEnSmDr5ZqUA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-react": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/@azure/msal-react/-/msal-react-5.4.2.tgz", + "integrity": "sha512-UK4xVqwhdi0qKebb76Z9oI6lLnNPmpcSdoBcBBnLao4+GXPJNA1HP2Tj5mWQSwd0EPTBCcV4msNiepPdxLk+ng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@azure/msal-browser": "^5.11.0", + "react": "^16.8.0 || ^17 || ^18 || ^19.2.1" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -435,9 +472,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ "ppc64" ], @@ -448,13 +485,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], @@ -465,13 +502,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], @@ -482,13 +519,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], @@ -499,13 +536,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], @@ -516,13 +553,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], @@ -533,13 +570,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], @@ -550,13 +587,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], @@ -567,13 +604,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], @@ -584,13 +621,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], @@ -601,13 +638,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], @@ -618,13 +655,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], @@ -635,13 +672,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], @@ -652,13 +689,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], @@ -669,13 +706,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], @@ -686,13 +723,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], @@ -703,13 +740,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -720,13 +757,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], @@ -737,13 +791,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], @@ -754,13 +825,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], @@ -771,13 +859,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], @@ -788,13 +876,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ "ia32" ], @@ -805,13 +893,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], @@ -822,7 +910,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -2459,46 +2547,6 @@ "node": ">= 0.4" } }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4371,130 +4419,951 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/vite-node/node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true }, "lightningcss": { "optional": true }, - "sass": { + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { "optional": true }, - "sass-embedded": { + "@vitest/browser": { "optional": true }, - "stylus": { + "@vitest/ui": { "optional": true }, - "sugarss": { + "happy-dom": { "optional": true }, - "terser": { + "jsdom": { "optional": true } } }, - "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, "node_modules/vitest/node_modules/@vitest/mocker": { @@ -4524,6 +5393,45 @@ } } }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "node_modules/vitest/node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", diff --git a/admin-webapp/package.json b/admin-webapp/package.json index 7494a9b..d07084b 100644 --- a/admin-webapp/package.json +++ b/admin-webapp/package.json @@ -12,6 +12,8 @@ "test:watch": "vitest" }, "dependencies": { + "@azure/msal-browser": "^5.11.0", + "@azure/msal-react": "^5.4.2", "react": "^19.2.6", "react-dom": "^19.2.6" }, diff --git a/admin-webapp/src/App.jsx b/admin-webapp/src/App.jsx index f2dc1f7..dc689c4 100644 --- a/admin-webapp/src/App.jsx +++ b/admin-webapp/src/App.jsx @@ -1,5 +1,9 @@ import { useState } from 'react' import BotsPage from './pages/BotsPage.jsx' +import OrdersPage from './pages/OrdersPage.jsx' +import AuthGate from './auth/AuthGate.jsx' +import UserMenu from './components/UserMenu.jsx' +import { authEnabled } from './auth/authConfig.js' const tabs = [ { id: 'bots', label: 'Bots' }, @@ -11,39 +15,42 @@ export default function App() { const [active, setActive] = useState('bots') return ( -
- +
+ {tabs.map((t) => ( + + ))} +
+ {authEnabled && } + -
- {active === 'bots' && } - {active === 'orders' && } - {active === 'config' && ( - - )} -
-
+
+ {active === 'bots' && } + {active === 'orders' && } + {active === 'config' && ( + + )} +
+ + ) } diff --git a/admin-webapp/src/api/admin.js b/admin-webapp/src/api/admin.js index eeba459..0f44589 100644 --- a/admin-webapp/src/api/admin.js +++ b/admin-webapp/src/api/admin.js @@ -11,6 +11,8 @@ import { updateBot as botnetUpdate, deleteBot as botnetDelete, listBots as botnetList, + rechargeBot as botnetRecharge, + updateServicingStatus as botnetServicing, } from './bots.js' import { @@ -73,4 +75,30 @@ export async function removeBot(id, name) { return { botnet: botnetResult, simulator: simResult } } +// #51 Quick-action: recharge. BotNet sets the battery to 100; mirror that to +// the simulator's powerLevel so the runtime bot reflects the recharge. +export async function rechargeBot(id, name) { + const botnetResult = await botnetRecharge(id) + if (botnetResult.error) { + return { botnet: botnetResult, simulator: null } + } + if (!simulatorConfig.configured) { + return { botnet: botnetResult, simulator: { ok: false, skipped: true } } + } + const botId = toBotId(name) + const simResult = await updateSimulatorBot(botId, { powerLevel: 100 }) + return { botnet: botnetResult, simulator: simResult } +} + +// #51 Quick-action: toggle servicing status. BotNet is the source of truth. +// The simulator's UpdateBotRequest exposes no settable status field (BotStatus +// is managed internally by the simulation), so this is a BotNet-only write. +export async function setServicingStatus(id, isServicingCustomer) { + const botnetResult = await botnetServicing(id, isServicingCustomer) + return { + botnet: botnetResult, + simulator: { ok: true, skipped: true, reason: 'Simulator has no settable status field' }, + } +} + export { simulatorConfig } diff --git a/admin-webapp/src/api/admin.test.js b/admin-webapp/src/api/admin.test.js index 3f1cf72..23a5fcd 100644 --- a/admin-webapp/src/api/admin.test.js +++ b/admin-webapp/src/api/admin.test.js @@ -5,6 +5,8 @@ vi.mock('./bots.js', () => ({ updateBot: vi.fn(), deleteBot: vi.fn(), listBots: vi.fn(), + rechargeBot: vi.fn(), + updateServicingStatus: vi.fn(), })) vi.mock('./simulator.js', () => ({ @@ -130,3 +132,60 @@ describe('modifyBot (issue #50)', () => { expect(result.simulator.skipped).toBe(true) }) }) + +describe('rechargeBot (issue #51)', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('recharges in BotNet, then sets the simulator powerLevel to 100', async () => { + bots.rechargeBot.mockResolvedValue({ source: 'api', data: { id: 5, batteryLevel: 100 } }) + sim.updateSimulatorBot.mockResolvedValue({ ok: true, data: {} }) + + const result = await admin.rechargeBot(5, 'Bot-005') + + expect(bots.rechargeBot).toHaveBeenCalledWith(5) + expect(sim.updateSimulatorBot).toHaveBeenCalledWith('bot-005', { powerLevel: 100 }) + expect(result.botnet.data.batteryLevel).toBe(100) + expect(result.simulator.ok).toBe(true) + }) + + it('surfaces a simulator failure as a partial-failure result', async () => { + bots.rechargeBot.mockResolvedValue({ source: 'api', data: { id: 6 } }) + sim.updateSimulatorBot.mockResolvedValue({ ok: false, error: '404 Not Found' }) + + const result = await admin.rechargeBot(6, 'Bot-006') + + expect(result.simulator.ok).toBe(false) + expect(result.simulator.error).toContain('404') + }) + + it('does not call the simulator if BotNet recharge errored', async () => { + bots.rechargeBot.mockResolvedValue({ source: 'api', error: '404 Not Found' }) + + const result = await admin.rechargeBot(99, 'ghost') + + expect(sim.updateSimulatorBot).not.toHaveBeenCalled() + expect(result.simulator).toBeNull() + }) +}) + +describe('setServicingStatus (issue #51)', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('writes to BotNet and skips the simulator (no settable status field)', async () => { + bots.updateServicingStatus.mockResolvedValue({ + source: 'api', + data: { id: 5, isServicingCustomer: true }, + }) + + const result = await admin.setServicingStatus(5, true) + + expect(bots.updateServicingStatus).toHaveBeenCalledWith(5, true) + expect(sim.updateSimulatorBot).not.toHaveBeenCalled() + expect(result.botnet.data.isServicingCustomer).toBe(true) + expect(result.simulator.skipped).toBe(true) + }) +}) diff --git a/admin-webapp/src/api/bots.js b/admin-webapp/src/api/bots.js index d255da3..eaf5d52 100644 --- a/admin-webapp/src/api/bots.js +++ b/admin-webapp/src/api/bots.js @@ -3,6 +3,8 @@ // If the API is unreachable, calls fall back to mock data so the admin app can // be demoed without the backend deployed. +import { getAuthHeaders } from '../auth/token.js' + const baseUrl = (import.meta.env.VITE_BOTNET_API_URL ?? '').replace(/\/+$/, '') const mockBots = [ @@ -45,7 +47,11 @@ async function callOrMock(path, init, mockResult) { return { data: mockResult, source: 'mock' } } try { - const res = await fetch(`${baseUrl}${path}`, init) + const authHeaders = await getAuthHeaders() + const res = await fetch(`${baseUrl}${path}`, { + ...init, + headers: { ...(init?.headers), ...authHeaders }, + }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = res.status === 204 ? null : await res.json() return { data, source: 'api' } diff --git a/admin-webapp/src/api/orders.js b/admin-webapp/src/api/orders.js new file mode 100644 index 0000000..8560830 --- /dev/null +++ b/admin-webapp/src/api/orders.js @@ -0,0 +1,90 @@ +// Order Service API client (issue #53). +// Contract from OrderService/OrderService/Controllers/OrdersController.cs. +// GET /api/orders (no customerId) returns all orders for the admin view. +// Falls back to clearly-labeled mock data when VITE_ORDER_SERVICE_URL is unset +// so the admin app can be demoed without the Order Service deployed. + +import { getAuthHeaders } from '../auth/token.js' + +const baseUrl = (import.meta.env.VITE_ORDER_SERVICE_URL ?? '').replace(/\/+$/, '') + +// Mirrors the OrderStatus enum in OrderService/Models/OrderStatus.cs. +export const ORDER_STATUSES = [ + 'Pending', + 'Assigned', + 'InTransit', + 'Delivered', + 'Cancelled', + 'Failed', +] + +const mockOrders = [ + { + id: '7c1f0a2e-1111-4a01-9b01-0a1b2c3d4e01', + customerId: 'Jane Doe:509-555-0101', + assignedBotId: 'bot-001', + status: 'InTransit', + deliveryAddress: '123 Main St, Spokane, WA', + destination: { latitude: 47.6588, longitude: -117.426 }, + items: [{ itemId: 'beverage', quantity: 2 }], + createdAt: '2026-06-01T18:42:11Z', + }, + { + id: '7c1f0a2e-2222-4a02-9b02-0a1b2c3d4e02', + customerId: 'Carlos Reyes:509-555-0188', + assignedBotId: 'bot-002', + status: 'Delivered', + deliveryAddress: '900 Riverside Ave, Spokane, WA', + destination: { latitude: 47.6601, longitude: -117.42 }, + items: [{ itemId: 'food', quantity: 1 }], + createdAt: '2026-06-01T17:05:48Z', + }, + { + id: '7c1f0a2e-3333-4a03-9b03-0a1b2c3d4e03', + customerId: 'Priya Singh:509-555-0143', + assignedBotId: null, + status: 'Pending', + deliveryAddress: '55 W Boone Ave, Spokane, WA', + destination: { latitude: 47.668, longitude: -117.41 }, + items: [{ itemId: 'package', quantity: 1 }], + createdAt: '2026-06-01T19:12:02Z', + }, + { + id: '7c1f0a2e-4444-4a04-9b04-0a1b2c3d4e04', + customerId: 'Sam Park:509-555-0120', + assignedBotId: 'bot-003', + status: 'Failed', + deliveryAddress: '1200 N Division St, Spokane, WA', + destination: { latitude: 47.67, longitude: -117.409 }, + items: [{ itemId: 'beverage', quantity: 3 }], + createdAt: '2026-05-31T22:30:15Z', + }, +] + +async function callOrMock(path, init, mockResult) { + if (!baseUrl) { + return { data: mockResult, source: 'mock' } + } + try { + const authHeaders = await getAuthHeaders() + const res = await fetch(`${baseUrl}${path}`, { + ...init, + headers: { ...(init?.headers), ...authHeaders }, + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data = res.status === 204 ? null : await res.json() + return { data, source: 'api' } + } catch (err) { + console.warn(`Order Service unreachable (${err.message}); using mock data.`) + return { data: mockResult, source: 'mock', error: err.message } + } +} + +export function listOrders() { + return callOrMock('/api/orders', undefined, mockOrders) +} + +export const apiConfig = { + baseUrl, + configured: Boolean(baseUrl), +} diff --git a/admin-webapp/src/api/orders.test.js b/admin-webapp/src/api/orders.test.js new file mode 100644 index 0000000..65f3d79 --- /dev/null +++ b/admin-webapp/src/api/orders.test.js @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, afterEach } from 'vitest' +import { ORDER_STATUSES } from './orders.js' + +describe('orders client (issue #53)', () => { + afterEach(() => { + vi.unstubAllEnvs() + vi.resetModules() + }) + + it('falls back to mock data when no Order Service URL is configured', async () => { + // Stub the env empty and re-import so the result is deterministic even when + // a local .env.local supplies a real URL. + vi.resetModules() + vi.stubEnv('VITE_ORDER_SERVICE_URL', '') + const mod = await import('./orders.js?nocache=' + Date.now()) + + expect(mod.apiConfig.configured).toBe(false) + + const { data, source } = await mod.listOrders() + expect(source).toBe('mock') + expect(Array.isArray(data)).toBe(true) + expect(data.length).toBeGreaterThan(0) + // Every mock order carries the fields the admin table renders. + for (const order of data) { + expect(order).toHaveProperty('id') + expect(order).toHaveProperty('customerId') + expect(order).toHaveProperty('status') + expect(order).toHaveProperty('createdAt') + } + }) + + it('exposes the OrderStatus values for the status filter', () => { + expect(ORDER_STATUSES).toContain('Pending') + expect(ORDER_STATUSES).toContain('InTransit') + expect(ORDER_STATUSES).toContain('Delivered') + expect(ORDER_STATUSES).toContain('Failed') + }) +}) diff --git a/admin-webapp/src/api/simulator.js b/admin-webapp/src/api/simulator.js index dadf3b5..9a28002 100644 --- a/admin-webapp/src/api/simulator.js +++ b/admin-webapp/src/api/simulator.js @@ -3,6 +3,8 @@ // Falls back to a no-op when VITE_SIMULATOR_API_URL is unset so the admin app // still works against BotNet alone. +import { getAuthHeaders } from '../auth/token.js' + const baseUrl = (import.meta.env.VITE_SIMULATOR_API_URL ?? '').replace(/\/+$/, '') // Spokane city center, used as the default location for newly registered bots. @@ -24,7 +26,11 @@ async function call(path, init) { return { ok: false, skipped: true, reason: 'Simulator URL not configured' } } try { - const res = await fetch(`${baseUrl}${path}`, init) + const authHeaders = await getAuthHeaders() + const res = await fetch(`${baseUrl}${path}`, { + ...init, + headers: { ...(init?.headers), ...authHeaders }, + }) if (!res.ok) { let detail = `HTTP ${res.status}` try { diff --git a/admin-webapp/src/auth/AuthGate.jsx b/admin-webapp/src/auth/AuthGate.jsx new file mode 100644 index 0000000..197353e --- /dev/null +++ b/admin-webapp/src/auth/AuthGate.jsx @@ -0,0 +1,101 @@ +import { MsalAuthenticationTemplate, useMsal } from '@azure/msal-react' +import { InteractionType } from '@azure/msal-browser' +import { authEnabled, loginRequest, ADMIN_GROUP_ID } from './authConfig.js' + +// Wraps the app. When auth is disabled, renders children as-is. When enabled, +// MsalAuthenticationTemplate drives an interactive redirect sign-in (it manages +// the in-progress state, so it won't loop), then GroupGate enforces membership +// in the DeliveryBot-Admin group before showing the app. +export default function AuthGate({ children }) { + if (!authEnabled) return children + + return ( + + {children} + + ) +} + +function GroupGate({ children }) { + const { instance, accounts } = useMsal() + const account = accounts[0] + const groups = account?.idTokenClaims?.groups ?? [] + const inAdminGroup = !ADMIN_GROUP_ID || groups.includes(ADMIN_GROUP_ID) + + if (!inAdminGroup) { + return ( + instance.logoutRedirect()} + /> + ) + } + + return children +} + +function Loading() { + return +} + +function ErrorScreen({ error }) { + return ( + + ) +} + +function Centered({ title, body, onSignOut }) { + return ( +
+
+ 🤖 +

{title}

+

{body}

+ {onSignOut && ( + + )} +
+
+ ) +} + +const styles = { + wrap: { + minHeight: '100vh', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '2rem', + }, + card: { + textAlign: 'center', + maxWidth: '24rem', + padding: '2.5rem', + border: '1px solid var(--border)', + borderRadius: '16px', + background: 'var(--bg-elev)', + }, + mark: { fontSize: '2.5rem' }, + title: { margin: '0.75rem 0 0.5rem', fontSize: '1.5rem' }, + body: { margin: 0, color: 'var(--text-dim)' }, + btn: { + marginTop: '1.25rem', + background: 'var(--accent)', + color: 'white', + border: 'none', + padding: '0.5rem 1rem', + borderRadius: '8px', + fontSize: '0.9rem', + }, +} diff --git a/admin-webapp/src/auth/authConfig.js b/admin-webapp/src/auth/authConfig.js new file mode 100644 index 0000000..e107582 --- /dev/null +++ b/admin-webapp/src/auth/authConfig.js @@ -0,0 +1,38 @@ +// Entra ID (Azure AD) configuration for staff sign-in (issue #54). +// +// Auth is gated behind env vars. While they're blank (e.g. before the Entra +// app registration exists) `authEnabled` is false and the admin app runs with +// auth disabled — so local dev and the current deployment keep working. Fill +// the VITE_ENTRA_* vars in (workflow env / .env.local) to switch auth on. + +const clientId = import.meta.env.VITE_ENTRA_CLIENT_ID ?? '' +const tenantId = import.meta.env.VITE_ENTRA_TENANT_ID ?? '' + +// Object ID of the DeliveryBot-Admin security group. When set, only members +// may use the app; when blank, any signed-in user is allowed. +export const ADMIN_GROUP_ID = import.meta.env.VITE_ENTRA_ADMIN_GROUP_ID ?? '' + +// Auth turns on only when both the client and tenant IDs are present. +export const authEnabled = Boolean(clientId && tenantId) + +const redirectUri = + typeof window !== 'undefined' ? window.location.origin : 'http://localhost:5173' + +export const msalConfig = { + auth: { + clientId, + authority: `https://login.microsoftonline.com/${tenantId}`, + redirectUri, + postLogoutRedirectUri: redirectUri, + }, + cache: { + cacheLocation: 'sessionStorage', + storeAuthStateInCookie: false, + }, +} + +// openid + profile yield the ID token (name) and group claims. Add API scopes +// here once the backends validate bearer tokens. +export const loginRequest = { + scopes: ['openid', 'profile', 'User.Read'], +} diff --git a/admin-webapp/src/auth/authConfig.test.js b/admin-webapp/src/auth/authConfig.test.js new file mode 100644 index 0000000..df38d32 --- /dev/null +++ b/admin-webapp/src/auth/authConfig.test.js @@ -0,0 +1,26 @@ +import { describe, it, expect, vi, afterEach } from 'vitest' + +// authEnabled is derived from env, so stub the env and re-import for a +// deterministic result regardless of any local .env.local. +describe('authConfig (issue #54)', () => { + afterEach(() => { + vi.unstubAllEnvs() + vi.resetModules() + }) + + it('disables auth when the Entra env vars are blank', async () => { + vi.resetModules() + vi.stubEnv('VITE_ENTRA_CLIENT_ID', '') + vi.stubEnv('VITE_ENTRA_TENANT_ID', '') + const mod = await import('./authConfig.js?nocache=' + Date.now()) + expect(mod.authEnabled).toBe(false) + }) + + it('enables auth when both client and tenant IDs are set', async () => { + vi.resetModules() + vi.stubEnv('VITE_ENTRA_CLIENT_ID', 'test-client-id') + vi.stubEnv('VITE_ENTRA_TENANT_ID', 'test-tenant-id') + const mod = await import('./authConfig.js?nocache=' + Date.now()) + expect(mod.authEnabled).toBe(true) + }) +}) diff --git a/admin-webapp/src/auth/msalInstance.js b/admin-webapp/src/auth/msalInstance.js new file mode 100644 index 0000000..c043235 --- /dev/null +++ b/admin-webapp/src/auth/msalInstance.js @@ -0,0 +1,6 @@ +import { PublicClientApplication } from '@azure/msal-browser' +import { authEnabled, msalConfig } from './authConfig.js' + +// A real MSAL instance is only constructed when auth is configured. While +// disabled this stays null and the app renders without an MsalProvider. +export const msalInstance = authEnabled ? new PublicClientApplication(msalConfig) : null diff --git a/admin-webapp/src/auth/token.js b/admin-webapp/src/auth/token.js new file mode 100644 index 0000000..60b89d6 --- /dev/null +++ b/admin-webapp/src/auth/token.js @@ -0,0 +1,23 @@ +import { authEnabled, loginRequest } from './authConfig.js' + +// Returns an Authorization header for outbound API calls when a user is signed +// in, or {} when auth is disabled / no token is available. The msal-browser +// module is only imported when auth is enabled, so it stays out of the test +// and mock-mode code paths. +export async function getAuthHeaders() { + if (!authEnabled) return {} + const { msalInstance } = await import('./msalInstance.js') + if (!msalInstance) return {} + + const account = msalInstance.getActiveAccount() ?? msalInstance.getAllAccounts()[0] + if (!account) return {} + + try { + const result = await msalInstance.acquireTokenSilent({ ...loginRequest, account }) + return { Authorization: `Bearer ${result.accessToken}` } + } catch { + // Silent acquisition can fail (e.g. expired session). Don't block the call; + // the backend simply receives no token. + return {} + } +} diff --git a/admin-webapp/src/components/UserMenu.jsx b/admin-webapp/src/components/UserMenu.jsx new file mode 100644 index 0000000..886e59e --- /dev/null +++ b/admin-webapp/src/components/UserMenu.jsx @@ -0,0 +1,33 @@ +import { useMsal } from '@azure/msal-react' + +// Shows the signed-in staff member's name and a sign-out control in the top +// nav. Only rendered when auth is enabled (inside the MsalProvider). +export default function UserMenu() { + const { instance, accounts } = useMsal() + const account = accounts[0] + const name = account?.name ?? account?.username ?? 'Signed in' + + return ( +
+ + {name} + + +
+ ) +} + +const styles = { + wrap: { display: 'flex', alignItems: 'center', gap: '0.6rem' }, + name: { color: 'var(--text)', fontSize: '0.9rem', fontWeight: 500 }, + btn: { + background: 'transparent', + color: 'var(--text-dim)', + border: '1px solid var(--border)', + padding: '0.4rem 0.75rem', + borderRadius: '8px', + fontSize: '0.85rem', + }, +} diff --git a/admin-webapp/src/main.jsx b/admin-webapp/src/main.jsx index b9a1a6d..8b68125 100644 --- a/admin-webapp/src/main.jsx +++ b/admin-webapp/src/main.jsx @@ -1,10 +1,51 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { MsalProvider } from '@azure/msal-react' +import { EventType } from '@azure/msal-browser' import './index.css' import App from './App.jsx' +import { authEnabled } from './auth/authConfig.js' +import { msalInstance } from './auth/msalInstance.js' -createRoot(document.getElementById('root')).render( - - - , -) +const root = createRoot(document.getElementById('root')) + +function render() { + root.render( + + {authEnabled && msalInstance ? ( + + + + ) : ( + + )} + , + ) +} + +if (authEnabled && msalInstance) { + // MSAL v5 requires initialize() before use. MsalProvider itself handles the + // returning redirect response — we must NOT also call handleRedirectPromise + // here, or the two race and the sign-in loops. We just keep the active + // account in sync from the cache and from successful logins. + msalInstance + .initialize() + .then(() => { + const accounts = msalInstance.getAllAccounts() + if (accounts.length > 0) { + msalInstance.setActiveAccount(accounts[0]) + } + msalInstance.addEventCallback((event) => { + if (event.eventType === EventType.LOGIN_SUCCESS && event.payload?.account) { + msalInstance.setActiveAccount(event.payload.account) + } + }) + render() + }) + .catch((err) => { + console.error(err) + render() + }) +} else { + render() +} diff --git a/admin-webapp/src/pages/BotsPage.jsx b/admin-webapp/src/pages/BotsPage.jsx index 9b9daa9..f4fe85d 100644 --- a/admin-webapp/src/pages/BotsPage.jsx +++ b/admin-webapp/src/pages/BotsPage.jsx @@ -4,9 +4,11 @@ import { registerBot, modifyBot, removeBot, + rechargeBot, + setServicingStatus, simulatorConfig, } from '../api/admin.js' -import { apiConfig as botnetConfig, rechargeBot, updateServicingStatus } from '../api/bots.js' +import { apiConfig as botnetConfig } from '../api/bots.js' import BotDialog from '../components/BotDialog.jsx' import ConfirmDialog from '../components/ConfirmDialog.jsx' @@ -45,16 +47,27 @@ export default function BotsPage() { refresh() }, [refresh]) - async function onRecharge(id) { - setBusyId(id) - await rechargeBot(id) + // #51 Quick-action: recharge (double-writes battery=100 to BotNet + simulator) + async function onRecharge(bot) { + setBusyId(bot.id) + const result = await rechargeBot(bot.id, bot.name) + const sim = result?.simulator + if (!result?.botnet?.error && sim && !sim.ok && !sim.skipped) { + setBanner({ + tone: 'warn', + text: `Bot #${bot.id} recharged in BotNet, but simulator sync failed: ${sim.error || 'unknown error'}.`, + }) + } else { + setBanner(null) + } await refresh() setBusyId(null) } + // #51 Quick-action: toggle servicing status (BotNet-only — see admin.js) async function onToggleServicing(bot) { setBusyId(bot.id) - await updateServicingStatus(bot.id, !bot.isServicingCustomer) + await setServicingStatus(bot.id, !bot.isServicingCustomer) await refresh() setBusyId(null) } @@ -209,7 +222,7 @@ export default function BotsPage() { + + + +
+ + + + + + + + + + + + {loading && visible.length === 0 && ( + + + + )} + {!loading && visible.length === 0 && ( + + + + )} + {visible.map((order) => { + const c = statusColor(order.status) + return ( + + + + + + + + ) + })} + +
Order IDCustomerAssigned BotStatusCreated
Loading orders…
+ {orders.length === 0 + ? 'No orders yet.' + : `No orders with status "${statusFilter}".`} +
+ {shortId(order.id)} + {customerName(order.customerId)}{order.assignedBotId || Unassigned} + + {order.status} + + {formatTime(order.createdAt)}
+
+ + + + ) +} + +const styles = { + section: { padding: '0 2rem 2rem' }, + header: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'flex-end', + gap: '1rem', + marginBottom: '1rem', + flexWrap: 'wrap', + }, + headerRight: { display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }, + h1: { margin: 0, fontSize: '1.75rem' }, + sub: { margin: '0.25rem 0 0', color: 'var(--text-dim)' }, + filterLabel: { color: 'var(--text-dim)', fontSize: '0.9rem' }, + select: { + background: 'var(--bg-elev)', + color: 'var(--text)', + border: '1px solid var(--border)', + padding: '0.4rem 0.6rem', + borderRadius: '8px', + fontSize: '0.9rem', + }, + secondaryBtn: { + background: 'transparent', + color: 'var(--text)', + border: '1px solid var(--border)', + padding: '0.5rem 1rem', + borderRadius: '8px', + fontSize: '0.9rem', + }, + tableWrap: { + background: 'var(--bg-elev)', + border: '1px solid var(--border)', + borderRadius: '12px', + overflow: 'hidden', + }, + table: { width: '100%', borderCollapse: 'collapse' }, + th: { + textAlign: 'left', + padding: '0.75rem 1rem', + background: 'var(--bg-elev-2)', + color: 'var(--text-dim)', + fontWeight: 500, + fontSize: '0.85rem', + textTransform: 'uppercase', + letterSpacing: '0.04em', + borderBottom: '1px solid var(--border)', + }, + td: { + padding: '0.75rem 1rem', + borderBottom: '1px solid var(--border)', + fontSize: '0.95rem', + }, + pill: { + padding: '0.2rem 0.6rem', + border: '1px solid', + borderRadius: '999px', + fontSize: '0.8rem', + }, + footer: { + marginTop: '1rem', + fontSize: '0.8rem', + color: 'var(--text-dim)', + display: 'flex', + gap: '1.5rem', + flexWrap: 'wrap', + }, + code: { + background: 'var(--bg-elev-2)', + border: '1px solid var(--border)', + padding: '0.15rem 0.4rem', + borderRadius: '4px', + fontFamily: 'ui-monospace, Consolas, monospace', + fontSize: '0.78rem', + }, +}