Bill Bot 1

This commit is contained in:
Sidharth Prabhu
2026-06-17 21:02:49 +05:30
parent df9f451fc8
commit a96e36e409
30 changed files with 1665 additions and 516 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
backend/.DS_Store vendored Normal file

Binary file not shown.

BIN
backend/src/.DS_Store vendored Normal file

Binary file not shown.

BIN
backend/src/main/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -51,8 +51,13 @@ public class SecurityConfig {
// ── PUBLIC: Terminal hardware order lookup (auth via X-API-KEY header, not JWT) ──
.requestMatchers(HttpMethod.GET, "/api/terminals/orders/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/terminals/validate").permitAll()
.requestMatchers(HttpMethod.POST, "/api/terminals/pair").permitAll()
.requestMatchers(HttpMethod.POST, "/api/terminals/*/verify-pin").permitAll()
// ── PUBLIC: Device log ingestion (ESP32 Bill-Bot devices, no JWT) ──
.requestMatchers(HttpMethod.POST, "/api/device-logs").permitAll()
// ── PUBLIC: Notifications read (admin frontend polls this before login guard kicks in) ──
.requestMatchers(HttpMethod.GET, "/api/notifications/**").permitAll()
@@ -66,10 +71,14 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.GET, "/api/orders/user/**").authenticated()
.requestMatchers(HttpMethod.GET, "/api/wallet/balance/**").authenticated()
.requestMatchers(HttpMethod.GET, "/api/wallet/transactions/**").authenticated()
.requestMatchers(HttpMethod.POST, "/api/wallet/topup").authenticated()
.requestMatchers(HttpMethod.POST, "/api/coupons/redeem").authenticated()
.requestMatchers(HttpMethod.POST, "/api/feedback/**").authenticated()
.requestMatchers(HttpMethod.GET, "/api/feedback/**").authenticated()
.requestMatchers(HttpMethod.GET, "/api/auth/user/**").authenticated()
.requestMatchers(HttpMethod.POST, "/api/auth/change-pin").authenticated()
.requestMatchers(HttpMethod.PUT, "/api/auth/users/*").authenticated()
.requestMatchers(HttpMethod.PUT, "/api/orders/*").authenticated()
// ── STAFF/MANAGER/MASTER: All other management APIs ──
.requestMatchers("/api/**").hasAnyRole("MASTER", "MANAGER", "STAFF", "OPERATOR")

View File

@@ -0,0 +1,55 @@
package com.rit.canteen.sales.controller;
import com.rit.canteen.sales.model.DeviceLog;
import com.rit.canteen.sales.service.DeviceLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/device-logs")
public class DeviceLogController {
@Autowired
private DeviceLogService deviceLogService;
/**
* POST /api/device-logs
* Accepts JSON: { "device_id": "...", "message": "..." }
* Called by ESP32 Bill-Bot devices to log events.
*/
@PostMapping
public ResponseEntity<Void> createLog(@RequestBody Map<String, String> body) {
String deviceId = body.get("device_id");
String message = body.get("message");
if (deviceId == null || message == null) {
return ResponseEntity.badRequest().build();
}
deviceLogService.saveLog(deviceId, message);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
/**
* GET /api/device-logs
* Returns all device logs (newest first) for admin dashboard.
*/
@GetMapping
public List<DeviceLog> getAllLogs() {
return deviceLogService.getAllLogs();
}
/**
* GET /api/device-logs/{deviceId}
* Returns logs for a specific device.
*/
@GetMapping("/{deviceId}")
public List<DeviceLog> getLogsByDevice(@PathVariable String deviceId) {
return deviceLogService.getLogsByDeviceId(deviceId);
}
}

View File

@@ -256,6 +256,11 @@ public class OrderController {
public ResponseEntity<?> updateOrder(@PathVariable Long id, @RequestBody Order updatedOrder) {
try {
return orderRepository.findById(id).map(existingOrder -> {
// ── SECURITY: Verify ownership of order ──
Long tokenUserId = getTokenUserId();
if (tokenUserId != null && !tokenUserId.equals(existingOrder.getUserId()) && !isStaff()) {
return ResponseEntity.status(403).body(Map.of("error", "Access denied"));
}
BigDecimal oldAmount = existingOrder.getTotalAmount();
BigDecimal newAmount = updatedOrder.getTotalAmount();

View File

@@ -3,6 +3,7 @@ package com.rit.canteen.sales.controller;
import com.rit.canteen.sales.model.Order;
import com.rit.canteen.sales.model.Terminal;
import com.rit.canteen.sales.model.TerminalDTO;
import com.rit.canteen.sales.service.DevicePairingService;
import com.rit.canteen.sales.service.TerminalService;
import com.rit.canteen.sales.repository.TerminalRepository;
import com.rit.canteen.sales.repository.OrderRepository;
@@ -22,6 +23,9 @@ public class TerminalController {
@Autowired
private TerminalService terminalService;
@Autowired
private DevicePairingService devicePairingService;
@Autowired
private TerminalRepository terminalRepository;
@@ -36,7 +40,10 @@ public class TerminalController {
t.getName(),
t.getLocation(),
"********",
"****"
"****",
t.isPaired(),
t.getDeviceId() != null ? maskDeviceId(t.getDeviceId()) : null,
t.getPairedAt()
))
.toList();
}
@@ -64,6 +71,10 @@ public class TerminalController {
return ResponseEntity.noContent().build();
}
// ──────────────────────────────────────────────────────────────
// Order Lookup (ESP32 uses X-API-KEY)
// ──────────────────────────────────────────────────────────────
@GetMapping("/orders/{orderNumber}")
public ResponseEntity<?> getOrderForTerminal(
@PathVariable String orderNumber,
@@ -97,4 +108,112 @@ public class TerminalController {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", "Order not found"));
}
}
@GetMapping("/validate")
public ResponseEntity<?> validateApiKey(@RequestHeader("X-API-KEY") String apiKey) {
Optional<Terminal> terminal = terminalRepository.findByApiKey(apiKey);
if (terminal.isEmpty()) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("status", "INVALID", "message", "Invalid API Key"));
}
Terminal t = terminal.get();
System.out.println(">>> Terminal Connected & Validated: " + t.getName() + " [ID: " + t.getId() + ", Location: " + t.getLocation() + "]");
return ResponseEntity.ok(Map.of(
"status", "VALID",
"terminalId", t.getId(),
"name", t.getName(),
"location", t.getLocation()
));
}
// ──────────────────────────────────────────────────────────────
// OTP-Based Device Pairing
// ──────────────────────────────────────────────────────────────
/**
* POST /api/terminals/pair
* Called by ESP32 during setup. The device sends its OTP and chipId.
* If pairing is already completed (admin linked it), returns the apiKey.
* Otherwise returns status "WAITING".
*
* No auth required — the device has no credentials yet.
*/
@PostMapping("/pair")
public ResponseEntity<?> registerDeviceOtp(@RequestBody Map<String, String> body) {
String otp = body.get("otp");
String deviceId = body.get("deviceId");
if (otp == null || otp.isBlank() || deviceId == null || deviceId.isBlank()) {
return ResponseEntity.badRequest().body(Map.of("message", "Both 'otp' and 'deviceId' are required"));
}
Map<String, Object> result = devicePairingService.registerOtp(otp, deviceId);
return ResponseEntity.ok(result);
}
/**
* POST /api/terminals/{id}/link-device
* Called by admin dashboard. Admin enters the OTP shown on the ESP32 screen.
* Links the device to this terminal record.
*
* Requires JWT auth (admin role).
*/
@PostMapping("/{id}/link-device")
public ResponseEntity<?> linkDevice(@PathVariable Long id, @RequestBody Map<String, String> body) {
String otp = body.get("otp");
if (otp == null || otp.isBlank()) {
return ResponseEntity.badRequest().body(Map.of("message", "OTP is required"));
}
Optional<Terminal> result = devicePairingService.linkDevice(id, otp);
if (result.isPresent()) {
Terminal t = result.get();
return ResponseEntity.ok(Map.of(
"message", "Device linked successfully",
"terminalId", t.getId(),
"name", t.getName(),
"deviceId", t.getDeviceId()
));
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("message", "Invalid or expired OTP. Please check the code on the device screen and try again."));
}
}
/**
* POST /api/terminals/{id}/unpair
* Called by admin dashboard to disconnect a device from a terminal.
* The device will need to re-pair on next boot.
*/
@PostMapping("/{id}/unpair")
public ResponseEntity<?> unpairDevice(@PathVariable Long id) {
Optional<Terminal> result = devicePairingService.unpairDevice(id);
if (result.isPresent()) {
return ResponseEntity.ok(Map.of("message", "Device unpaired successfully"));
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("message", "Terminal not found"));
}
}
// ──────────────────────────────────────────────────────────────
// Helpers
// ──────────────────────────────────────────────────────────────
/**
* Masks a device ID for display: "Esp 32 - AABBCCDDEEFF" → "Esp 32 - AA••••••EEFF"
*/
private String maskDeviceId(String deviceId) {
if (deviceId == null || deviceId.length() < 8) return deviceId;
// Find the last part (MAC portion) of the device ID
int dashIndex = deviceId.lastIndexOf('-');
if (dashIndex >= 0 && dashIndex + 5 < deviceId.length()) {
String prefix = deviceId.substring(0, dashIndex + 1).trim();
String mac = deviceId.substring(dashIndex + 1).trim();
if (mac.length() >= 6) {
return prefix + " " + mac.substring(0, 2) + "••••" + mac.substring(mac.length() - 4);
}
}
return deviceId.substring(0, 4) + "••••" + deviceId.substring(deviceId.length() - 4);
}
}

View File

@@ -128,6 +128,11 @@ public class UserController {
public ResponseEntity<LoginResponse.UserDto> updateUser(
@PathVariable Long id,
@Valid @RequestBody UserUpdateRequest request) {
// Extra ownership check: customers can only update their own profile; staff can update any
Long tokenUserId = getTokenUserId();
if (tokenUserId != null && !tokenUserId.equals(id) && !isStaff()) {
return ResponseEntity.status(403).build();
}
LoginResponse.UserDto updated = userService.updateUser(id, request.getName(),
request.getMobileNumber(), request.getPin());
return updated != null ? ResponseEntity.ok(updated) : ResponseEntity.notFound().build();
@@ -147,6 +152,17 @@ public class UserController {
// ── Helpers ──────────────────────────────────────────────────────────────
private Long getTokenUserId() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getDetails() instanceof Claims claims) {
Object uid = claims.get("userId");
if (uid != null) {
return uid instanceof Integer ? ((Integer) uid).longValue() : (Long) uid;
}
}
return null;
}
private String getAuthenticatedMobile() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getDetails() instanceof Claims claims) {

View File

@@ -74,6 +74,9 @@ public class WalletController {
public ResponseEntity<?> topUp(@RequestBody Map<String, Object> request) {
try {
Long userId = Long.valueOf(request.get("userId").toString());
if (!canAccessUser(userId)) {
return ResponseEntity.status(403).body(Map.of("error", "Access denied"));
}
BigDecimal amount = new BigDecimal(request.get("amount").toString());
// ── FIX: validate amount BEFORE touching the database ──

View File

@@ -0,0 +1,47 @@
package com.rit.canteen.sales.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "device_logs")
public class DeviceLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "device_id", nullable = false)
private String deviceId;
@Column(nullable = false, length = 2000)
private String message;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
}
public DeviceLog() {}
public DeviceLog(String deviceId, String message) {
this.deviceId = deviceId;
this.message = message;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getDeviceId() { return deviceId; }
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}

View File

@@ -2,6 +2,7 @@ package com.rit.canteen.sales.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDateTime;
@Entity
@Table(name = "terminals")
@@ -23,6 +24,15 @@ public class Terminal {
@Column(unique = true)
private String apiKey;
@Column(name = "device_id")
private String deviceId;
@Column(nullable = false, columnDefinition = "boolean default false")
private boolean paired = false;
@Column(name = "paired_at")
private LocalDateTime pairedAt;
public Terminal() {}
public Terminal(String name, String location, String pin, String apiKey) {
@@ -47,4 +57,13 @@ public class Terminal {
public String getApiKey() { return apiKey; }
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
public String getDeviceId() { return deviceId; }
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
public boolean isPaired() { return paired; }
public void setPaired(boolean paired) { this.paired = paired; }
public LocalDateTime getPairedAt() { return pairedAt; }
public void setPairedAt(LocalDateTime pairedAt) { this.pairedAt = pairedAt; }
}

View File

@@ -1,20 +1,29 @@
package com.rit.canteen.sales.model;
import java.time.LocalDateTime;
public class TerminalDTO {
private Long id;
private String name;
private String location;
private String apiKey;
private String pin;
private boolean paired;
private String deviceId;
private LocalDateTime pairedAt;
public TerminalDTO() {}
public TerminalDTO(Long id, String name, String location, String apiKey, String pin) {
public TerminalDTO(Long id, String name, String location, String apiKey, String pin,
boolean paired, String deviceId, LocalDateTime pairedAt) {
this.id = id;
this.name = name;
this.location = location;
this.apiKey = apiKey;
this.pin = pin;
this.paired = paired;
this.deviceId = deviceId;
this.pairedAt = pairedAt;
}
// Getters and Setters
@@ -32,4 +41,13 @@ public class TerminalDTO {
public String getPin() { return pin; }
public void setPin(String pin) { this.pin = pin; }
public boolean isPaired() { return paired; }
public void setPaired(boolean paired) { this.paired = paired; }
public String getDeviceId() { return deviceId; }
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
public LocalDateTime getPairedAt() { return pairedAt; }
public void setPairedAt(LocalDateTime pairedAt) { this.pairedAt = pairedAt; }
}

View File

@@ -0,0 +1,11 @@
package com.rit.canteen.sales.repository;
import com.rit.canteen.sales.model.DeviceLog;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface DeviceLogRepository extends JpaRepository<DeviceLog, Long> {
List<DeviceLog> findAllByOrderByCreatedAtDesc();
List<DeviceLog> findByDeviceIdOrderByCreatedAtDesc(String deviceId);
}

View File

@@ -0,0 +1,30 @@
package com.rit.canteen.sales.service;
import com.rit.canteen.sales.model.DeviceLog;
import com.rit.canteen.sales.repository.DeviceLogRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class DeviceLogService {
@Autowired
private DeviceLogRepository deviceLogRepository;
@Transactional
public DeviceLog saveLog(String deviceId, String message) {
DeviceLog log = new DeviceLog(deviceId, message);
return deviceLogRepository.save(log);
}
public List<DeviceLog> getAllLogs() {
return deviceLogRepository.findAllByOrderByCreatedAtDesc();
}
public List<DeviceLog> getLogsByDeviceId(String deviceId) {
return deviceLogRepository.findByDeviceIdOrderByCreatedAtDesc(deviceId);
}
}

View File

@@ -0,0 +1,151 @@
package com.rit.canteen.sales.service;
import com.rit.canteen.sales.model.Terminal;
import com.rit.canteen.sales.repository.TerminalRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* Manages the in-memory OTP pairing lifecycle for ESP32 terminal devices.
*
* Flow:
* 1. Device boots, generates a 6-digit OTP, displays it on screen
* 2. Device calls registerOtp() — stores OTP + deviceId in memory (5-min TTL)
* 3. Admin sees OTP on device screen, enters it in dashboard against a terminal
* 4. Admin calls linkDevice() — matches OTP, binds device to terminal, returns apiKey
* 5. Device polls checkPairingStatus() — gets back apiKey once admin completes step 4
*/
@Service
public class DevicePairingService {
private static final long OTP_TTL_MS = 5 * 60 * 1000L; // 5 minutes
@Autowired
private TerminalRepository terminalRepository;
/**
* Represents a pending pairing request from a device.
*/
private static class PairingRequest {
final String otp;
final String deviceId;
final Instant createdAt;
// Set once admin links the device
volatile String apiKey;
volatile Long terminalId;
volatile boolean completed;
PairingRequest(String otp, String deviceId) {
this.otp = otp;
this.deviceId = deviceId;
this.createdAt = Instant.now();
this.completed = false;
}
boolean isExpired() {
return Instant.now().toEpochMilli() - createdAt.toEpochMilli() > OTP_TTL_MS;
}
}
// OTP → PairingRequest
private final ConcurrentHashMap<String, PairingRequest> pendingPairings = new ConcurrentHashMap<>();
/**
* Called by the ESP32 device. Registers (or refreshes) the device's OTP in memory.
* If the OTP was already matched by an admin, returns the assigned apiKey.
*
* @return Map with status ("WAITING" or "PAIRED") and optionally "apiKey"
*/
public Map<String, Object> registerOtp(String otp, String deviceId) {
PairingRequest existing = pendingPairings.get(otp);
// If this OTP was already linked by admin, return the apiKey
if (existing != null && existing.completed && existing.deviceId.equals(deviceId)) {
// Clean up after delivering the key
pendingPairings.remove(otp);
return Map.of(
"status", "PAIRED",
"apiKey", existing.apiKey,
"terminalId", existing.terminalId
);
}
// Register or refresh the OTP
pendingPairings.put(otp, new PairingRequest(otp, deviceId));
return Map.of("status", "WAITING");
}
/**
* Called by the admin dashboard. Links a device's OTP to a specific terminal.
*
* @return Optional.empty() if OTP not found/expired, or the updated Terminal
*/
public Optional<Terminal> linkDevice(Long terminalId, String otp) {
PairingRequest request = pendingPairings.get(otp);
if (request == null || request.isExpired()) {
pendingPairings.remove(otp); // Clean up expired
return Optional.empty();
}
Optional<Terminal> terminalOpt = terminalRepository.findById(terminalId);
if (terminalOpt.isEmpty()) {
return Optional.empty();
}
Terminal terminal = terminalOpt.get();
// Bind the device to the terminal
terminal.setDeviceId(request.deviceId);
terminal.setPaired(true);
terminal.setPairedAt(LocalDateTime.now());
terminalRepository.save(terminal);
// Mark the pairing as completed so the device can pick up the apiKey
request.apiKey = terminal.getApiKey();
request.terminalId = terminal.getId();
request.completed = true;
System.out.println(">>> Device paired: " + request.deviceId +
" → Terminal: " + terminal.getName() + " [ID: " + terminal.getId() + "]");
return Optional.of(terminal);
}
/**
* Unpairs a device from a terminal, clearing its deviceId and paired status.
*/
public Optional<Terminal> unpairDevice(Long terminalId) {
Optional<Terminal> terminalOpt = terminalRepository.findById(terminalId);
if (terminalOpt.isEmpty()) {
return Optional.empty();
}
Terminal terminal = terminalOpt.get();
terminal.setDeviceId(null);
terminal.setPaired(false);
terminal.setPairedAt(null);
terminalRepository.save(terminal);
System.out.println(">>> Device unpaired from Terminal: " + terminal.getName() +
" [ID: " + terminal.getId() + "]");
return Optional.of(terminal);
}
/**
* Periodic cleanup of expired OTP entries (runs every 60 seconds).
*/
@Scheduled(fixedRate = 60000)
public void cleanupExpiredOtps() {
pendingPairings.entrySet().removeIf(entry -> entry.getValue().isExpired());
}
}

View File

@@ -1,13 +1,14 @@
spring.application.name=backend
server.address=0.0.0.0
server.port=8080
# ============================================================
# DATABASE — REQUIRED environment variables
# Set these in your environment or a .env file
# ============================================================
spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/positeasy}
spring.datasource.username=${DB_USER:postgres}
spring.datasource.password=${DB_PASSWORD:}
spring.datasource.username=postgres
spring.datasource.password=sidharth
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=update

View File

@@ -0,0 +1,327 @@
# Terminal Integration & ESP32 Firmware API Specification
This document provides the API specifications of the Spring Boot backend for integrating terminal hardware (ESP32 controllers that scan QR codes and print bills).
---
## 1. Authentication
### For Paired Devices
Once a device is paired, all terminal-specific APIs use **API Key authentication**.
- **Header Key**: `X-API-KEY`
- **Value**: `POS-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` (Auto-generated when a terminal is created).
### For Unpaired Devices (During Pairing)
The pairing endpoint requires no authentication — the device has no credentials yet.
---
## 2. Device Onboarding (OTP-Based Pairing)
### Flow Overview
1. **Admin** creates a terminal in the dashboard (name, location, PIN)
2. **ESP32** boots with no stored API key → enters pairing mode
3. **ESP32** generates a 6-digit OTP and displays it on screen
4. **ESP32** POSTs the OTP + its chipId to `POST /api/terminals/pair`
5. **Admin** enters the OTP in the dashboard against the specific terminal
6. **Backend** matches the OTP, binds the device to the terminal
7. **ESP32** polls `POST /api/terminals/pair` again → receives `"PAIRED"` + API key
8. **ESP32** saves the API key to NVS flash and restarts
9. All subsequent communication uses `X-API-KEY` header — no further pairing needed
### A. Register Device OTP (ESP32 → Backend)
Used by the ESP32 to register its OTP and poll for pairing completion.
* **URL**: `/api/terminals/pair`
* **Method**: `POST`
* **Auth**: None (public endpoint)
* **Headers**:
* `Content-Type`: `application/json`
* **Request Body**:
```json
{
"otp": "482916",
"deviceId": "Esp 32 - AABBCCDDEEFF"
}
```
#### Response — Waiting (Admin hasn't linked yet):
```json
{
"status": "WAITING"
}
```
#### Response — Paired (Admin completed linking):
```json
{
"status": "PAIRED",
"apiKey": "POS-EF9832B743CA90B2381F0A1B2C3D4E5F",
"terminalId": 1
}
```
* **Response Codes**:
* `200 OK`: Always — check `status` field for state.
* `400 BAD_REQUEST`: Missing `otp` or `deviceId`.
---
### B. Link Device (Admin Dashboard → Backend)
Used by the admin to match a device's OTP to a terminal.
* **URL**: `/api/terminals/{id}/link-device`
* **Method**: `POST`
* **Auth**: JWT (admin role)
* **Headers**:
* `Content-Type`: `application/json`
* `Authorization`: `Bearer <jwt>`
* **Path Parameters**:
* `id` (Long): The terminal ID to link the device to.
* **Request Body**:
```json
{
"otp": "482916"
}
```
#### Response (`200 OK`):
```json
{
"message": "Device linked successfully",
"terminalId": 1,
"name": "Counter 1 Printer",
"deviceId": "Esp 32 - AABBCCDDEEFF"
}
```
* **Response Codes**:
* `200 OK`: Device linked successfully.
* `400 BAD_REQUEST`: Invalid or expired OTP.
---
### C. Unpair Device (Admin Dashboard → Backend)
Used by the admin to disconnect a device from a terminal.
* **URL**: `/api/terminals/{id}/unpair`
* **Method**: `POST`
* **Auth**: JWT (admin role)
#### Response (`200 OK`):
```json
{
"message": "Device unpaired successfully"
}
```
---
## 3. Operational Endpoints
### D. Get Order Details
Used by the ESP32 to fetch the order details after scanning a QR code.
* **URL**: `/api/terminals/orders/{orderNumber}`
* **Method**: `GET`
* **Headers**:
* `X-API-KEY`: `<YOUR_TERMINAL_API_KEY>`
* **Path Parameters**:
* `orderNumber` (String): The order ID scanned from the QR code (e.g., `ORD-87A3B2D9`).
#### Response Codes:
* `200 OK`: Order found and is eligible for processing.
* `401 UNAUTHORIZED`: Invalid or missing `X-API-KEY`.
* `404 NOT_FOUND`: Order number does not exist.
* `400 BAD_REQUEST`: Order has already been fulfilled (`status` is `COMPLETED`).
* `410 GONE`: Order has expired/archived.
#### Response Body (`200 OK` JSON Schema):
```json
{
"id": 12,
"orderNumber": "ORD-68EF73C9",
"displayOrderId": "045",
"userId": 5,
"totalAmount": 250.00,
"status": "PAID",
"paymentMethod": "RITZ_TOKEN",
"createdAt": "2026-06-17T13:30:00",
"orderType": "STORE_ORDER",
"archived": false,
"hasFeedback": false,
"items": [
{
"id": 24,
"productId": 3,
"productName": "Masala Dosa",
"price": 60.00,
"quantity": 2,
"stallId": 1,
"stallName": "RIT Canteen"
},
{
"id": 25,
"productId": 7,
"productName": "Cold Coffee",
"price": 40.00,
"quantity": 1,
"stallId": 2,
"stallName": "Juice Bar"
}
]
}
```
---
### E. Validate API Key
Used by the ESP32 to verify its API key is still valid (optional health check).
* **URL**: `/api/terminals/validate`
* **Method**: `GET`
* **Headers**:
* `X-API-KEY`: `<YOUR_TERMINAL_API_KEY>`
#### Response (`200 OK`):
```json
{
"status": "VALID",
"terminalId": 1,
"name": "Counter 1 Printer",
"location": "Main Hall"
}
```
---
### F. Device Logging
Used by the ESP32 to submit runtime system logs to the backend.
* **URL**: `/api/device-logs`
* **Method**: `POST`
* **Headers**:
* `Content-Type`: `application/json`
* **Request Body**:
```json
{
"device_id": "Esp 32 - AABBCCDDEEFF",
"message": "Firmware Version: 1.2.6"
}
```
* **Response Codes**:
* `201 CREATED`: Log successfully recorded.
* `400 BAD_REQUEST`: Missing `device_id` or `message`.
---
### G. Verify Terminal PIN (Admin Tool)
Used during admin operations to reveal the terminal's API Key.
* **URL**: `/api/terminals/{id}/verify-pin`
* **Method**: `POST`
* **Headers**:
* `Content-Type`: `application/json`
* **Path Parameters**:
* `id` (Long): The database ID of the terminal registration.
* **Request Body**:
```json
{
"pin": "1234"
}
```
* **Response Body (`200 OK`):**
```json
{
"id": 1,
"name": "Counter 1 Printer",
"location": "Main Hall",
"pin": "1234",
"apiKey": "POS-EF9832B743CA90B2381F",
"deviceId": "Esp 32 - AABBCCDDEEFF",
"paired": true,
"pairedAt": "2026-06-17T14:00:00"
}
```
---
## 4. ESP32 Firmware Integration Guide
### Pairing Flow (`database.cpp`)
The ESP32 now uses OTP-based pairing instead of JWT token polling:
```cpp
void setupPairing()
{
// 1. Generate 6-digit OTP
currentOtp = generateUniqueOTP(oldOtp);
displayStatus("OTP: " + currentOtp + "\nEnter on Admin Panel", 300);
// 2. POST OTP + deviceId to /api/terminals/pair
JsonDocument requestDoc;
requestDoc["otp"] = currentOtp;
requestDoc["deviceId"] = getDeviceId();
// 3. Poll every 3 seconds — check if status == "PAIRED"
// 4. On success, extract apiKey and save to NVS
apiKey = doc["apiKey"].as<String>();
saveApiKey(apiKey);
ESP.restart();
}
```
### Data Fetching (`database.cpp`)
All requests use `X-API-KEY` instead of `Authorization: Bearer`:
```cpp
String getData(String orderNumber)
{
httpClient.addHeader("X-API-KEY", apiKey); // Not Bearer token
// ... GET /api/terminals/orders/{orderNumber}
}
```
### Order JSON Parsing (`bill_bot_system.cpp`)
The new backend returns order details at the **root level** (no `"data"` wrapper):
| Old Path | New Path |
|---|---|
| `doc["data"]["orderId"]` | `doc["displayOrderId"]` or `doc["orderNumber"]` |
| `doc["data"]["status"]` | `doc["status"]` (`"PAID"` = ready, `"COMPLETED"` = already done) |
| `doc["data"]["dateTz"]` | `doc["createdAt"]` |
| `doc["data"]["order"]` (items) | `doc["items"]` |
| `item["productInfo"]["name"]` | `item["productName"]` |
| `item["counterId"]` | `item["stallId"]` / `item["stallName"]` |
| `item["price"]` (in paise) | `item["price"]` (in rupees, no /100 needed) |
### Device Logging (`database.cpp`)
Logs now go to the Spring Boot backend directly (no Supabase):
```cpp
void sendLog(String message)
{
httpClient.begin(logUrl); // /api/device-logs
httpClient.addHeader("Content-Type", "application/json");
// No supabaseApiKey header needed
httpClient.POST(body);
}
```
### QR Code Format
The QR code payload is now the **order number directly**:
- Old: `prefix$PAYMENT_ID$suffix` → parsed with `$` delimiters
- New: `ORD-87A3B2D9` → passed directly to `getData()`

View File

@@ -56,13 +56,13 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/helper-validator-identifier": "^7.29.7",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -71,9 +71,9 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -81,21 +81,21 @@
}
},
"node_modules/@babel/core": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.28.6",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
"@babel/helper-compilation-targets": "^7.29.7",
"@babel/helper-module-transforms": "^7.29.7",
"@babel/helpers": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/template": "^7.29.7",
"@babel/traverse": "^7.29.7",
"@babel/types": "^7.29.7",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -112,14 +112,14 @@
}
},
"node_modules/@babel/generator": {
"version": "7.29.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/parser": "^7.29.7",
"@babel/types": "^7.29.7",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -129,14 +129,14 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1",
"@babel/compat-data": "^7.29.7",
"@babel/helper-validator-option": "^7.29.7",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
@@ -146,9 +146,9 @@
}
},
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -156,29 +156,29 @@
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.28.6",
"@babel/types": "^7.28.6"
"@babel/traverse": "^7.29.7",
"@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.28.6",
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/traverse": "^7.28.6"
"@babel/helper-module-imports": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7",
"@babel/traverse": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
@@ -188,9 +188,9 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -198,9 +198,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -208,9 +208,9 @@
}
},
"node_modules/@babel/helper-validator-option": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -218,27 +218,27 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
"integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.28.6",
"@babel/types": "^7.29.0"
"@babel/template": "^7.29.7",
"@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.0"
"@babel/types": "^7.29.7"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -257,33 +257,33 @@
}
},
"node_modules/@babel/template": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.28.6",
"@babel/types": "^7.28.6"
"@babel/code-frame": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
"integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/types": "^7.29.0",
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
"@babel/helper-globals": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/template": "^7.29.7",
"@babel/types": "^7.29.7",
"debug": "^4.3.1"
},
"engines": {
@@ -291,23 +291,23 @@
}
},
"node_modules/@babel/types": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -317,9 +317,9 @@
}
},
"node_modules/@emnapi/runtime": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -598,14 +598,14 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
"integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==",
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.1"
"@tybys/wasm-util": "^0.10.2"
},
"funding": {
"type": "github",
@@ -617,9 +617,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.123.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.123.0.tgz",
"integrity": "sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew==",
"version": "0.133.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -663,9 +663,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.13.tgz",
"integrity": "sha512-5ZiiecKH2DXAVJTNN13gNMUcCDg4Jy8ZjbXEsPnqa248wgOVeYRX0iqXXD5Jz4bI9BFHgKsI2qmyJynstbmr+g==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
"cpu": [
"arm64"
],
@@ -680,9 +680,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.13.tgz",
"integrity": "sha512-tz/v/8G77seu8zAB3A5sK3UFoOl06zcshEzhUO62sAEtrEuW/H1CcyoupOrD+NbQJytYgA4CppXPzlrmp4JZKA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
"cpu": [
"arm64"
],
@@ -697,9 +697,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.13.tgz",
"integrity": "sha512-8DakphqOz8JrMYWTJmWA+vDJxut6LijZ8Xcdc4flOlAhU7PNVwo2MaWBF9iXjJAPo5rC/IxEFZDhJ3GC7NHvug==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
"cpu": [
"x64"
],
@@ -714,9 +714,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.13.tgz",
"integrity": "sha512-4wBQFfjDuXYN/SVI8inBF3Aa+isq40rc6VMFbk5jcpolUBTe5cYnMsHZ51nFWsx3PVyyNN3vgoESki0Hmr/4BA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
"cpu": [
"x64"
],
@@ -731,9 +731,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.13.tgz",
"integrity": "sha512-JW/e4yPIXLms+jmnbwwy5LA/LxVwZUWLN8xug+V200wzaVi5TEGIWQlh8o91gWYFxW609euI98OCCemmWGuPrw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
"cpu": [
"arm"
],
@@ -748,13 +748,16 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.13.tgz",
"integrity": "sha512-ZfKWpXiUymDnavepCaM6KG/uGydJ4l2nBmMxg60Ci4CbeefpqjPWpfaZM7PThOhk2dssqBAcwLc6rAyr0uTdXg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -765,13 +768,16 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.13.tgz",
"integrity": "sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -782,13 +788,16 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.13.tgz",
"integrity": "sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -799,13 +808,16 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.13.tgz",
"integrity": "sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -816,13 +828,16 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.13.tgz",
"integrity": "sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -833,13 +848,16 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.13.tgz",
"integrity": "sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -850,9 +868,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.13.tgz",
"integrity": "sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
"cpu": [
"arm64"
],
@@ -867,9 +885,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.13.tgz",
"integrity": "sha512-viLS5C5et8NFtLWw9Sw3M/w4vvnVkbWkO7wSNh3C+7G1+uCkGpr6PcjNDSFcNtmXY/4trjPBqUfcOL+P3sWy/g==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
"cpu": [
"wasm32"
],
@@ -877,52 +895,18 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.9.1",
"@emnapi/runtime": "1.9.1",
"@napi-rs/wasm-runtime": "^1.1.2"
"@emnapi/core": "1.10.0",
"@emnapi/runtime": "1.10.0",
"@napi-rs/wasm-runtime": "^1.1.4"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
"integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.0",
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.13.tgz",
"integrity": "sha512-Fqa3Tlt1xL4wzmAYxGNFV36Hb+VfPc9PYU+E25DAnswXv3ODDu/yyWjQDbXMo5AGWkQVjLgQExuVu8I/UaZhPQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
"cpu": [
"arm64"
],
@@ -937,9 +921,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.13.tgz",
"integrity": "sha512-/pLI5kPkGEi44TDlnbio3St/5gUFeN51YWNAk/Gnv6mEQBOahRBh52qVFVBpmrnU01n2yysvBML9Ynu7K4kGAQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
"cpu": [
"x64"
],
@@ -1259,9 +1243,9 @@
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -1607,9 +1591,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2245,9 +2229,9 @@
}
},
"node_modules/dompurify": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz",
"integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==",
"version": "3.4.10",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.10.tgz",
"integrity": "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
@@ -2855,10 +2839,20 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -3313,9 +3307,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true,
"funding": [
{
@@ -3462,9 +3456,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
"funding": [
{
@@ -3482,7 +3476,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -3588,9 +3582,9 @@
}
},
"node_modules/react-router": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz",
"integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==",
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz",
"integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -3610,12 +3604,12 @@
}
},
"node_modules/react-router-dom": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz",
"integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==",
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz",
"integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==",
"license": "MIT",
"dependencies": {
"react-router": "7.14.0"
"react-router": "7.18.0"
},
"engines": {
"node": ">=20.0.0"
@@ -3704,14 +3698,14 @@
}
},
"node_modules/rolldown": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.13.tgz",
"integrity": "sha512-bvVj8YJmf0rq4pSFmH7laLa6pYrhghv3PRzrCdRAr23g66zOKVJ4wkvFtgohtPLWmthgg8/rkaqRHrpUEh0Zbw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.123.0",
"@rolldown/pluginutils": "1.0.0-rc.13"
"@oxc-project/types": "=0.133.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
"rolldown": "bin/cli.mjs"
@@ -3720,27 +3714,27 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.13",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.13",
"@rolldown/binding-darwin-x64": "1.0.0-rc.13",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.13",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.13",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.13",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.13",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.13",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.13",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.13",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.13",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.13",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.13",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.13",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.13"
"@rolldown/binding-android-arm64": "1.0.3",
"@rolldown/binding-darwin-arm64": "1.0.3",
"@rolldown/binding-darwin-x64": "1.0.3",
"@rolldown/binding-freebsd-x64": "1.0.3",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
"@rolldown/binding-linux-arm64-musl": "1.0.3",
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
"@rolldown/binding-linux-x64-gnu": "1.0.3",
"@rolldown/binding-linux-x64-musl": "1.0.3",
"@rolldown/binding-openharmony-arm64": "1.0.3",
"@rolldown/binding-wasm32-wasi": "1.0.3",
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
"@rolldown/binding-win32-x64-msvc": "1.0.3"
}
},
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.13",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz",
"integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
@@ -3893,14 +3887,14 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
@@ -4069,17 +4063,17 @@
}
},
"node_modules/vite": {
"version": "8.0.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.7.tgz",
"integrity": "sha512-P1PbweD+2/udplnThz3btF4cf6AgPky7kk23RtHUkJIU5BIxwPprhRGmOAHs6FTI7UiGbTNrgNP6jSYD6JaRnw==",
"version": "8.0.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.8",
"rolldown": "1.0.0-rc.13",
"tinyglobby": "^0.2.15"
"postcss": "^8.5.15",
"rolldown": "1.0.3",
"tinyglobby": "^0.2.17"
},
"bin": {
"vite": "bin/vite.js"
@@ -4095,7 +4089,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.0",
"@vitejs/devtools": "^0.1.18",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",

View File

@@ -0,0 +1,195 @@
import { apiFetch } from '../api';
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Wifi, CheckCircle2, AlertCircle, Smartphone } from 'lucide-react';
import Numpad from './Numpad.tsx';
interface LinkDeviceModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
terminalId: number | null;
terminalName: string;
}
const LinkDeviceModal: React.FC<LinkDeviceModalProps> = ({
isOpen,
onClose,
onSuccess,
terminalId,
terminalName
}) => {
const [otp, setOtp] = useState('');
const [success, setSuccess] = useState(false);
const [linkedDeviceId, setLinkedDeviceId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleLink = async (freshOtp?: string) => {
if (!terminalId) return;
const otpToSubmit = freshOtp || otp;
if (!otpToSubmit || otpToSubmit.length < 6) return;
setLoading(true);
setError(null);
try {
const response = await apiFetch(`/api/terminals/${terminalId}/link-device`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ otp: otpToSubmit }),
});
const data = await response.json();
if (response.ok) {
setSuccess(true);
setLinkedDeviceId(data.deviceId || null);
onSuccess();
} else {
setError(data.message || 'Failed to link device');
setOtp('');
}
} catch (err) {
setError('Connection error. Please try again.');
} finally {
setLoading(false);
}
};
const handleClose = () => {
setOtp('');
setSuccess(false);
setLinkedDeviceId(null);
setError(null);
onClose();
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm">
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="bg-white rounded-3xl shadow-2xl w-full max-w-md overflow-hidden relative"
>
<div className="p-6 border-b border-gray-100 flex items-center justify-between">
<h3 className="text-xl font-bold text-[#231651]">Link Physical Device</h3>
<button onClick={handleClose} className="p-2 hover:bg-gray-100 rounded-full transition-colors">
<X size={20} className="text-gray-400" />
</button>
</div>
<div className="p-8">
<AnimatePresence mode="wait">
{!success ? (
<motion.div
key="otp-entry"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.05 }}
className="space-y-6 text-center"
>
<div className="inline-flex p-4 bg-indigo-50 text-indigo-600 rounded-2xl mb-2">
<Wifi size={32} />
</div>
<div>
<h4 className="text-lg font-bold text-gray-900">{terminalName}</h4>
<p className="text-sm text-gray-500 mt-1">
Enter the 6-digit OTP shown on the device screen
</p>
</div>
{/* Step-by-step instructions */}
<div className="p-4 bg-gray-50 rounded-2xl text-left space-y-2">
<div className="flex items-start gap-3">
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-[#231651] text-white text-[10px] font-bold flex items-center justify-center mt-0.5">1</span>
<p className="text-xs text-gray-600 leading-relaxed">Power on the ESP32 device and connect it to Wi-Fi</p>
</div>
<div className="flex items-start gap-3">
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-[#231651] text-white text-[10px] font-bold flex items-center justify-center mt-0.5">2</span>
<p className="text-xs text-gray-600 leading-relaxed">A 6-digit OTP will appear on the device display</p>
</div>
<div className="flex items-start gap-3">
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-[#231651] text-white text-[10px] font-bold flex items-center justify-center mt-0.5">3</span>
<p className="text-xs text-gray-600 leading-relaxed">Enter that OTP below to link the device</p>
</div>
</div>
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="p-3 bg-red-50 text-red-600 rounded-xl text-sm font-medium flex items-center justify-center gap-2"
>
<AlertCircle size={16} />
{error}
</motion.div>
)}
<Numpad value={otp} onChange={(v) => {
setOtp(v);
if (v.length === 6) {
setTimeout(() => handleLink(v), 100);
}
}} maxLength={6} />
<button
disabled={otp.length < 6 || loading}
onClick={() => handleLink()}
className="w-full h-14 bg-[#231651] text-white rounded-2xl font-bold shadow-lg shadow-[#231651]/20 hover:scale-[1.02] active:scale-[0.98] transition-all disabled:opacity-50 disabled:scale-100"
>
{loading ? 'Linking Device...' : 'Link Device'}
</button>
</motion.div>
) : (
<motion.div
key="success"
initial={{ opacity: 0, scale: 1.1 }}
animate={{ opacity: 1, scale: 1 }}
className="space-y-8 text-center"
>
<div className="inline-flex p-4 bg-green-50 text-green-600 rounded-full mb-2">
<CheckCircle2 size={40} />
</div>
<div>
<h4 className="text-xl font-bold text-gray-900">Device Linked Successfully!</h4>
<p className="text-sm text-gray-500 mt-1">
The physical device is now paired with <span className="font-semibold">{terminalName}</span>
</p>
</div>
{linkedDeviceId && (
<div className="p-4 bg-gray-50 border border-gray-200 rounded-2xl">
<div className="flex items-center justify-center gap-2 text-sm text-gray-600">
<Smartphone size={16} />
<span className="font-mono font-medium">{linkedDeviceId}</span>
</div>
</div>
)}
<div className="p-4 bg-green-50/50 rounded-2xl flex gap-3 text-left">
<CheckCircle2 size={20} className="text-green-600 shrink-0 mt-0.5" />
<p className="text-[12px] text-green-700 leading-relaxed font-medium">
The device will automatically receive its API key and restart. It's now ready to scan QR codes and print bills.
</p>
</div>
<button
onClick={handleClose}
className="w-full h-14 bg-[#231651] text-white rounded-2xl font-bold shadow-lg shadow-[#231651]/20 hover:scale-[1.02] active:scale-[0.98] transition-all"
>
Done
</button>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
</div>
);
};
export default LinkDeviceModal;

View File

@@ -1,4 +1,4 @@
import { apiFetch } from '../api';
import { apiFetch } from '../api';
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import {
@@ -10,16 +10,25 @@ import {
Search,
ExternalLink,
Info,
Key
Key,
Wifi,
WifiOff,
LinkIcon,
Unlink,
Smartphone
} from 'lucide-react';
import AddTerminalModal from '../components/AddTerminalModal.tsx';
import PinVerificationModal from '../components/PinVerificationModal.tsx';
import LinkDeviceModal from '../components/LinkDeviceModal.tsx';
interface Terminal {
id: number;
name: string;
location: string;
apiKey: string;
paired: boolean;
deviceId: string | null;
pairedAt: string | null;
}
const Terminals = () => {
@@ -28,6 +37,7 @@ const Terminals = () => {
const [searchQuery, setSearchQuery] = useState('');
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
const [isLinkModalOpen, setIsLinkModalOpen] = useState(false);
const [selectedTerminal, setSelectedTerminal] = useState<Terminal | null>(null);
const fetchTerminals = async () => {
@@ -61,11 +71,26 @@ const Terminals = () => {
}
};
const handleUnpair = async (id: number, e: React.MouseEvent) => {
e.stopPropagation();
if (window.confirm('Unpair this device? It will need to be re-paired on next boot.')) {
try {
const response = await apiFetch(`/api/terminals/${id}/unpair`, { method: 'POST' });
if (response.ok) fetchTerminals();
} catch (error) {
console.error('Failed to unpair device:', error);
}
}
};
const pairedCount = terminals.filter(t => t.paired).length;
const filteredTerminals = terminals.filter(t => {
const query = (searchQuery || '').toLowerCase();
return (t.name || '').toLowerCase().includes(query) ||
(t.location || '').toLowerCase().includes(query);
});
return (
<div className="p-8 max-w-7xl mx-auto space-y-8 font-inter">
{/* Header Section */}
@@ -94,26 +119,28 @@ const Terminals = () => {
<Monitor size={24} />
</div>
<div>
<p className="text-white/60 text-sm font-medium">Active Devices</p>
<p className="text-white/60 text-sm font-medium">Registered Devices</p>
<p className="text-2xl font-bold">{terminals.length}</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-3xl border border-gray-100 shadow-sm flex items-center gap-4">
<div className="p-3 bg-green-50 text-green-600 rounded-2xl">
<ShieldCheck size={24} />
<div className={`p-3 rounded-2xl ${pairedCount > 0 ? 'bg-green-50 text-green-600' : 'bg-gray-50 text-gray-400'}`}>
<Wifi size={24} />
</div>
<div>
<p className="text-gray-500 text-sm font-medium">System Status</p>
<p className="text-xl font-bold text-gray-900 border-b-2 border-green-500 inline-block leading-tight">Secure & Online</p>
<p className="text-gray-500 text-sm font-medium">Paired Devices</p>
<p className="text-xl font-bold text-gray-900">
{pairedCount} <span className="text-sm font-medium text-gray-400">/ {terminals.length}</span>
</p>
</div>
</div>
<div className="bg-blue-50 p-6 rounded-3xl flex items-center gap-4 border border-blue-100">
<Info size={24} className="text-blue-600 shrink-0" />
<p className="text-sm text-blue-700 font-medium leading-relaxed">
Terminals are used at physical counters to scan order QRs and print bills.
Power on a device and enter its 6-digit OTP here to pair it with a terminal.
</p>
</div>
</div>
@@ -159,15 +186,30 @@ const Terminals = () => {
<div className="relative z-10 space-y-4">
<div className="flex items-start justify-between">
<div className="p-4 bg-gray-50 text-[#231651] rounded-2xl border border-gray-200 group-hover:bg-[#231651] group-hover:text-white transition-colors duration-300">
<div className={`p-4 rounded-2xl border transition-colors duration-300 ${
terminal.paired
? 'bg-green-50 text-green-600 border-green-200 group-hover:bg-green-600 group-hover:text-white'
: 'bg-gray-50 text-[#231651] border-gray-200 group-hover:bg-[#231651] group-hover:text-white'
}`}>
<Monitor size={28} />
</div>
<button
onClick={(e) => handleDelete(terminal.id, e)}
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-all"
>
<Trash2 size={18} />
</button>
<div className="flex items-center gap-1">
{terminal.paired && (
<button
onClick={(e) => handleUnpair(terminal.id, e)}
title="Unpair device"
className="p-2 text-gray-400 hover:text-orange-500 hover:bg-orange-50 rounded-xl transition-all"
>
<Unlink size={16} />
</button>
)}
<button
onClick={(e) => handleDelete(terminal.id, e)}
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-all"
>
<Trash2 size={18} />
</button>
</div>
</div>
<div>
@@ -178,16 +220,48 @@ const Terminals = () => {
</div>
</div>
{/* Device ID for paired terminals */}
{terminal.paired && terminal.deviceId && (
<div className="flex items-center gap-2 text-xs text-gray-400 font-mono bg-gray-50 px-3 py-1.5 rounded-lg w-fit">
<Smartphone size={12} />
<span>{terminal.deviceId}</span>
</div>
)}
<div className="pt-4 flex items-center justify-between border-t border-gray-50">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-[12px] font-bold text-gray-500 uppercase tracking-wider">Device Ready</span>
</div>
<div className="flex items-center gap-2 text-[#231651] font-bold text-sm">
<Key size={16} />
<span>View API Key</span>
<ExternalLink size={14} />
{terminal.paired ? (
<>
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-[12px] font-bold text-green-600 uppercase tracking-wider">Paired</span>
</>
) : (
<>
<div className="w-2 h-2 rounded-full bg-gray-300" />
<span className="text-[12px] font-bold text-gray-400 uppercase tracking-wider">Unpaired</span>
</>
)}
</div>
{terminal.paired ? (
<div className="flex items-center gap-2 text-[#231651] font-bold text-sm">
<Key size={16} />
<span>View API Key</span>
<ExternalLink size={14} />
</div>
) : (
<button
onClick={(e) => {
e.stopPropagation();
setSelectedTerminal(terminal);
setIsLinkModalOpen(true);
}}
className="flex items-center gap-2 text-indigo-600 font-bold text-sm hover:text-indigo-700 transition-colors bg-indigo-50 px-3 py-1.5 rounded-xl hover:bg-indigo-100"
>
<LinkIcon size={14} />
<span>Link Device</span>
</button>
)}
</div>
</div>
</motion.div>
@@ -226,6 +300,17 @@ const Terminals = () => {
terminalName={selectedTerminal?.name || ''}
/>
<LinkDeviceModal
isOpen={isLinkModalOpen}
onClose={() => {
setIsLinkModalOpen(false);
setSelectedTerminal(null);
}}
onSuccess={fetchTerminals}
terminalId={selectedTerminal?.id || null}
terminalName={selectedTerminal?.name || ''}
/>
<style dangerouslySetInnerHTML={{ __html: `
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
.font-inter { font-family: 'Inter', sans-serif; }

View File

@@ -31,13 +31,13 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/helper-validator-identifier": "^7.29.7",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -46,9 +46,9 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -56,21 +56,21 @@
}
},
"node_modules/@babel/core": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.28.6",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
"@babel/helper-compilation-targets": "^7.29.7",
"@babel/helper-module-transforms": "^7.29.7",
"@babel/helpers": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/template": "^7.29.7",
"@babel/traverse": "^7.29.7",
"@babel/types": "^7.29.7",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -87,14 +87,14 @@
}
},
"node_modules/@babel/generator": {
"version": "7.29.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/parser": "^7.29.7",
"@babel/types": "^7.29.7",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -104,14 +104,14 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1",
"@babel/compat-data": "^7.29.7",
"@babel/helper-validator-option": "^7.29.7",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
@@ -121,9 +121,9 @@
}
},
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -131,29 +131,29 @@
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.28.6",
"@babel/types": "^7.28.6"
"@babel/traverse": "^7.29.7",
"@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.28.6",
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/traverse": "^7.28.6"
"@babel/helper-module-imports": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7",
"@babel/traverse": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
@@ -163,9 +163,9 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -173,9 +173,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -183,9 +183,9 @@
}
},
"node_modules/@babel/helper-validator-option": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -193,27 +193,27 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
"integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.28.6",
"@babel/types": "^7.29.0"
"@babel/template": "^7.29.7",
"@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.0"
"@babel/types": "^7.29.7"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -223,33 +223,33 @@
}
},
"node_modules/@babel/template": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.28.6",
"@babel/types": "^7.28.6"
"@babel/code-frame": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
"integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/types": "^7.29.0",
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
"@babel/helper-globals": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/template": "^7.29.7",
"@babel/types": "^7.29.7",
"debug": "^4.3.1"
},
"engines": {
@@ -257,40 +257,38 @@
}
},
"node_modules/@babel/types": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -302,7 +300,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -567,14 +564,14 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
"integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==",
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.1"
"@tybys/wasm-util": "^0.10.2"
},
"funding": {
"type": "github",
@@ -586,9 +583,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.122.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
"integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
"version": "0.133.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -596,9 +593,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz",
"integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
"cpu": [
"arm64"
],
@@ -613,9 +610,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz",
"integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
"cpu": [
"arm64"
],
@@ -630,9 +627,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz",
"integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
"cpu": [
"x64"
],
@@ -647,9 +644,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz",
"integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
"cpu": [
"x64"
],
@@ -664,9 +661,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz",
"integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
"cpu": [
"arm"
],
@@ -681,9 +678,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz",
"integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
"cpu": [
"arm64"
],
@@ -701,9 +698,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz",
"integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
"cpu": [
"arm64"
],
@@ -721,9 +718,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz",
"integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
"cpu": [
"ppc64"
],
@@ -741,9 +738,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz",
"integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
"cpu": [
"s390x"
],
@@ -761,9 +758,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz",
"integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
"cpu": [
"x64"
],
@@ -781,9 +778,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz",
"integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
"cpu": [
"x64"
],
@@ -801,9 +798,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz",
"integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
"cpu": [
"arm64"
],
@@ -818,9 +815,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz",
"integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
"cpu": [
"wasm32"
],
@@ -828,16 +825,18 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@napi-rs/wasm-runtime": "^1.1.1"
"@emnapi/core": "1.10.0",
"@emnapi/runtime": "1.10.0",
"@napi-rs/wasm-runtime": "^1.1.4"
},
"engines": {
"node": ">=14.0.0"
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz",
"integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
"cpu": [
"arm64"
],
@@ -852,9 +851,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz",
"integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
"cpu": [
"x64"
],
@@ -876,9 +875,9 @@
"license": "MIT"
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -1129,9 +1128,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1322,9 +1321,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.16",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz",
"integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==",
"version": "2.10.37",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz",
"integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -1390,9 +1389,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001786",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz",
"integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==",
"version": "1.0.30001799",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
"integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
"dev": true,
"funding": [
{
@@ -1532,9 +1531,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.332",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.332.tgz",
"integrity": "sha512-7OOtytmh/rINMLwaFTbcMVvYXO3AUm029X0LcyfYk0B557RlPkdpTpnH9+htMlfu5dKwOmT0+Zs2Aw+lnn6TeQ==",
"version": "1.5.375",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz",
"integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==",
"dev": true,
"license": "ISC"
},
@@ -2015,10 +2014,20 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -2449,9 +2458,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true,
"funding": [
{
@@ -2475,11 +2484,14 @@
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.37",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
"integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
"version": "2.0.47",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
"integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
"dev": true,
"license": "MIT"
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/optionator": {
"version": "0.9.4",
@@ -2585,9 +2597,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
"funding": [
{
@@ -2605,7 +2617,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -2664,9 +2676,9 @@
}
},
"node_modules/react-router": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz",
"integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==",
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz",
"integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -2686,12 +2698,12 @@
}
},
"node_modules/react-router-dom": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz",
"integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==",
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz",
"integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==",
"license": "MIT",
"dependencies": {
"react-router": "7.14.0"
"react-router": "7.18.0"
},
"engines": {
"node": ">=20.0.0"
@@ -2712,14 +2724,14 @@
}
},
"node_modules/rolldown": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz",
"integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.122.0",
"@rolldown/pluginutils": "1.0.0-rc.12"
"@oxc-project/types": "=0.133.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
"rolldown": "bin/cli.mjs"
@@ -2728,27 +2740,27 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.12",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.12",
"@rolldown/binding-darwin-x64": "1.0.0-rc.12",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.12",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.12",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.12",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.12",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
"@rolldown/binding-android-arm64": "1.0.3",
"@rolldown/binding-darwin-arm64": "1.0.3",
"@rolldown/binding-darwin-x64": "1.0.3",
"@rolldown/binding-freebsd-x64": "1.0.3",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
"@rolldown/binding-linux-arm64-musl": "1.0.3",
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
"@rolldown/binding-linux-x64-gnu": "1.0.3",
"@rolldown/binding-linux-x64-musl": "1.0.3",
"@rolldown/binding-openharmony-arm64": "1.0.3",
"@rolldown/binding-wasm32-wasi": "1.0.3",
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
"@rolldown/binding-win32-x64-msvc": "1.0.3"
}
},
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz",
"integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
@@ -2834,14 +2846,14 @@
}
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
@@ -2969,17 +2981,17 @@
}
},
"node_modules/vite": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.5.tgz",
"integrity": "sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==",
"version": "8.0.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.8",
"rolldown": "1.0.0-rc.12",
"tinyglobby": "^0.2.15"
"postcss": "^8.5.15",
"rolldown": "1.0.3",
"tinyglobby": "^0.2.17"
},
"bin": {
"vite": "bin/vite.js"
@@ -2995,7 +3007,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.0",
"@vitejs/devtools": "^0.1.18",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Star, Send, X } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Star } from 'lucide-react';
import { motion } from 'framer-motion';
import './FeedbackModal.css';
interface OrderItem {

View File

@@ -5,7 +5,7 @@ interface AuthContextType {
user: User | null;
isLoading: boolean;
checkUserExists: (mobileNumber: string) => Promise<{ success: boolean; userExists: boolean; message: string }>;
login: (mobileNumber: string, pin: string) => Promise<{ success: boolean; message: string }>;
login: (mobileNumber: string, pin: string) => Promise<{ success: boolean; message: string; isSuspended?: boolean }>;
register: (mobileNumber: string, name: string, pin: string) => Promise<{ success: boolean; message: string }>;
logout: () => void;
changePin: (currentPin: string, newPin: string) => Promise<{ success: boolean; message: string }>;
@@ -53,9 +53,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (data.success && data.user) {
setUser(data.user);
localStorage.setItem('user', JSON.stringify(data.user));
if (data.token) {
localStorage.setItem('token', data.token);
}
return { success: true, message: data.message };
}
return { success: false, message: data.message || 'Login failed' };
return { success: false, message: data.message || 'Login failed', isSuspended: data.suspended || data.isSuspended };
} catch (error) {
return { success: false, message: 'Network error. Please try again.' };
}
@@ -72,6 +75,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (data.success && data.user) {
setUser(data.user);
localStorage.setItem('user', JSON.stringify(data.user));
if (data.token) {
localStorage.setItem('token', data.token);
}
return { success: true, message: data.message };
}
return { success: false, message: data.message || 'Registration failed' };
@@ -82,22 +88,31 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const logout = () => {
if (user) {
const token = localStorage.getItem('token');
fetch(`${API_BASE_URL}/logout`, { cache: 'no-store',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: JSON.stringify({ mobileNumber: user.mobileNumber }),
});
}
setUser(null);
localStorage.removeItem('user');
localStorage.removeItem('token');
};
const changePin = async (currentPin: string, newPin: string) => {
if (!user) return { success: false, message: 'Not logged in' };
try {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/change-pin`, { cache: 'no-store',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: JSON.stringify({ mobileNumber: user.mobileNumber, currentPin, newPin }),
});
const data = await response.json();
@@ -110,9 +125,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const updateProfile = async (name: string, mobileNumber: string) => {
if (!user) return { success: false, message: 'Not logged in' };
try {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/users/${user.id}`, { cache: 'no-store',
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: JSON.stringify({ name, mobileNumber }),
});
@@ -141,7 +160,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const refreshUser = async () => {
if (!user) return;
try {
const response = await fetch(`${API_BASE_URL}/user/${user.mobileNumber}`, { cache: 'no-store' });
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/user/${user.mobileNumber}`, {
cache: 'no-store',
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
});
if (response.ok) {
const data = await response.json();
const updatedUser: User = {

View File

@@ -7,7 +7,7 @@ interface FoodContextType {
stalls: Stall[];
isLoading: boolean;
error: string | null;
refreshData: () => Promise<void>;
refreshData: (silent?: boolean) => Promise<void>;
}
const FoodContext = createContext<FoodContextType | undefined>(undefined);

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
CircleDollarSign,
Wallet,
ChevronRight,
AlertCircle,
@@ -24,7 +23,6 @@ const CheckoutScreen: React.FC = () => {
const { refreshData } = useFood();
const [isProcessing, setIsProcessing] = useState(false);
const [currentBalance, setCurrentBalance] = useState<number>(user?.ritzTokenBalance || 0);
const [isLoadingBalance, setIsLoadingBalance] = useState(true);
// Conflict state
const [stockConflicts, setStockConflicts] = useState<any[]>([]);
@@ -37,14 +35,16 @@ const CheckoutScreen: React.FC = () => {
const fetchBalance = async () => {
if (!user) return;
try {
setIsLoadingBalance(true);
const response = await fetch(`http://${window.location.hostname}:8080/api/wallet/balance/${user.id}`);
const token = localStorage.getItem('token');
const response = await fetch(`http://${window.location.hostname}:8080/api/wallet/balance/${user.id}`, {
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
});
const data = await response.json();
setCurrentBalance(data.balance || 0);
} catch (error) {
console.error('Error fetching balance:', error);
} finally {
setIsLoadingBalance(false);
}
};
@@ -76,9 +76,13 @@ const CheckoutScreen: React.FC = () => {
};
try {
const token = localStorage.getItem('token');
const response = await fetch(`http://${window.location.hostname}:8080/api/orders`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: JSON.stringify(orderData),
});

View File

@@ -35,7 +35,12 @@ const HomeScreen: React.FC = () => {
if (sessionStorage.getItem('feedback_dismissed')) return;
try {
const response = await fetch(`http://${window.location.hostname}:8080/api/feedback/latest-unrated/${user?.id}`);
const token = localStorage.getItem('token');
const response = await fetch(`http://${window.location.hostname}:8080/api/feedback/latest-unrated/${user?.id}`, {
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
});
if (response.status === 200) {
const order = await response.json();
console.log('Unrated order found:', order);
@@ -53,8 +58,12 @@ const HomeScreen: React.FC = () => {
if (!unratedOrder) return;
try {
const token = localStorage.getItem('token');
await fetch(`http://${window.location.hostname}:8080/api/feedback/skip/${unratedOrder.id}`, {
method: 'POST'
method: 'POST',
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
});
setShowFeedbackModal(false);
setShowFeedbackSnackbar(false);
@@ -70,9 +79,13 @@ const HomeScreen: React.FC = () => {
const handleFeedbackSubmit = async (feedbackData: any) => {
try {
const token = localStorage.getItem('token');
const response = await fetch(`http://${window.location.hostname}:8080/api/feedback/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: JSON.stringify(feedbackData)
});
@@ -112,7 +125,7 @@ const HomeScreen: React.FC = () => {
<div className="container" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 24, textAlign: 'center' }}>
<h2 style={{ marginBottom: 12 }}>Oops! Something went wrong</h2>
<p style={{ color: 'var(--text-mid)', marginBottom: 24 }}>{error}</p>
<button className="primary-button" onClick={refreshData} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<button className="primary-button" onClick={() => refreshData()} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<RefreshCcw size={18} /> Try Again
</button>
</div>

View File

@@ -1,4 +1,5 @@
import React from 'react';
import type { FoodItem } from '../types';
import { useParams } from 'react-router-dom';
import { AlertCircle } from 'lucide-react';
import Header from '../components/Header';
@@ -56,21 +57,18 @@ const ItemDetailScreen: React.FC = () => {
fetchItem();
}
}, [itemId, contextItem]);
const quantity = item ? getItemQuantity(item.id) : 0;
const isLimitReached = item && item.stock !== undefined && quantity >= item.stock && item.stock > 0;
if ((isGlobalLoading && foodItems.length === 0) || isFetching) {
return (
<div className="container item-loading-wrapper">
<div className="loading-spinner-wrapper">
<div className="loading-spinner"></div>
<p>Loading Delights...</p>
if (isGlobalLoading || isFetching || !item) {
if (isGlobalLoading || isFetching) {
return (
<div className="container item-loading-wrapper">
<div className="loading-spinner-wrapper">
<div className="loading-spinner"></div>
<p>Loading Delights...</p>
</div>
</div>
</div>
);
}
);
}
if (!item && !isGlobalLoading && !isFetching) {
return (
<div className="container item-not-found-wrapper">
<Header />
@@ -84,6 +82,9 @@ const ItemDetailScreen: React.FC = () => {
);
}
const quantity = getItemQuantity(item.id);
const isLimitReached = item.stock !== undefined && quantity >= item.stock && item.stock > 0;
return (
<div className={`container item-detail-page ${item.stock === 0 ? 'out-of-stock' : ''}`}>
<Header title="" />

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Clock, ShoppingBag as ShoppingBagIcon, ChevronRight as ChevronRightIcon, X, RefreshCcw, AlertCircle, ShoppingCart } from 'lucide-react';
import { Clock, ShoppingBag as ShoppingBagIcon, ChevronRight as ChevronRightIcon, X, RefreshCcw, AlertCircle } from 'lucide-react';
import { QRCodeCanvas } from 'qrcode.react';
import { motion, AnimatePresence } from 'framer-motion';
import Header from '../components/Header';
@@ -58,7 +58,7 @@ const MyOrdersScreen: React.FC = () => {
// Polling for live updates (Sync when Dashboard regenerates QR or changes status)
useEffect(() => {
let interval: NodeJS.Timeout;
let interval: ReturnType<typeof setInterval>;
// Only poll if there are active (non-finalized) orders
const hasActiveOrders = orders.some(o =>
@@ -79,7 +79,12 @@ const MyOrdersScreen: React.FC = () => {
const fetchOrders = async () => {
try {
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/user/${user?.id}`);
const token = localStorage.getItem('token');
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/user/${user?.id}`, {
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
});
const data = await response.json();
setOrders(data);
} catch (error) {
@@ -132,10 +137,14 @@ const MyOrdersScreen: React.FC = () => {
if (!selectedOrder) return;
setIsRegenerating(true);
try {
const token = localStorage.getItem('token');
const newOrderNumber = `ORD-${Math.random().toString(36).substring(2, 10).toUpperCase()}`;
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/${selectedOrder.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: JSON.stringify({
...selectedOrder,
orderNumber: newOrderNumber

View File

@@ -71,9 +71,9 @@ const StallDetailScreen: React.FC = () => {
if (!prevStall) return prevStall;
return {
...prevStall,
products: prevStall.products.map(p =>
products: prevStall.products ? prevStall.products.map(p =>
p.id === update.productId.toString() ? { ...p, stock: update.stock } : p
)
) : []
};
});
} catch (err) {