diff --git a/web-backend/src/main/java/com/statusneo/vms/config/ValidationConfig.java b/web-backend/src/main/java/com/statusneo/vms/config/ValidationConfig.java new file mode 100644 index 0000000..950607a --- /dev/null +++ b/web-backend/src/main/java/com/statusneo/vms/config/ValidationConfig.java @@ -0,0 +1,24 @@ +package com.statusneo.vms.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; + + +@Configuration +public class ValidationConfig { + + + @Bean + public LocalValidatorFactoryBean validator() { + return new LocalValidatorFactoryBean(); + } + + @Bean + public MethodValidationPostProcessor methodValidationPostProcessor() { + MethodValidationPostProcessor processor = new MethodValidationPostProcessor(); + processor.setValidator(validator()); + return processor; + } +} \ No newline at end of file diff --git a/web-backend/src/main/java/com/statusneo/vms/controller/VisitorController.java b/web-backend/src/main/java/com/statusneo/vms/controller/VisitorController.java index 391fba7..9adc462 100644 --- a/web-backend/src/main/java/com/statusneo/vms/controller/VisitorController.java +++ b/web-backend/src/main/java/com/statusneo/vms/controller/VisitorController.java @@ -17,7 +17,7 @@ * under the License. */ package com.statusneo.vms.controller; - +import org.springframework.validation.FieldError; import com.statusneo.vms.cache.EmployeeNameCache; import com.statusneo.vms.dto.VerificationResult; import com.statusneo.vms.model.Visit; @@ -27,12 +27,14 @@ import com.statusneo.vms.service.GraphDirectoryService; import com.statusneo.vms.service.OtpService; import com.statusneo.vms.service.VisitService; +import jakarta.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; @@ -61,119 +63,118 @@ public class VisitorController { @Autowired private EmployeeRepository employeeRepository; - - @GetMapping("/report") - public ResponseEntity getReport(@RequestParam String period) { - List visit; - if (period.equals("daily")) { - visit = visitRepository.findAllByVisitDateBetween(LocalDateTime.now().toLocalDate().atStartOfDay(), LocalDateTime.now()); - } else if (period.equals("monthly")) { - visit = visitRepository.findAllByVisitDateBetween(LocalDateTime.now().minusMonths(1), LocalDateTime.now()); - } else { - return ResponseEntity.badRequest().body("Invalid period"); - } - return ResponseEntity.ok(visit); - } - -// @RequestMapping("/error") - public String handleError() { - return "Custom error page!"; - } - @GetMapping("/") - public String home() { - return "index"; // Looks for src/main/resources/templates/simple.html - } - - - @GetMapping("/search") - public String searchEmployees(@RequestParam("employee") String query, Model model) { - logger.info("Received search request for employee: {}", query); - List names = employeeNameCache.getEmployeeNamesByPrefix(query == null ? "" : query); - model.addAttribute("employees", names); - return "employeeSearchResults"; + public String home(Model model) { + model.addAttribute("visitor", new Visitor()); + return "index"; } - @GetMapping("/refresh-employee-cache") - public ResponseEntity refreshEmployeeCache() { - employeeNameCache.initializeCache(); - return ResponseEntity.ok("Cache refreshed"); + // Add this method to serve the registration form + @GetMapping("/register") + public String showRegistrationForm(Model model) { + model.addAttribute("visitor", new Visitor()); + model.addAttribute("employees", employeeRepository.findAll()); // or employeeService.getAllEmployees() + return "visitorRegistration"; } @PostMapping("/register") - public String registerVisitor(@ModelAttribute Visitor visitor, - @RequestParam(value = "host", required = false) String host, - @RequestParam(value = "employee", required = false) String employee, - @RequestHeader(value = "HX-Request", required = false) String hxRequest, - Model model) { - // prefer explicit host id, fall back to name + public String registerVisitor( + @Valid @ModelAttribute("visitor") Visitor visitor, + BindingResult bindingResult, + @RequestParam(value = "host", required = false) String host, + @RequestParam(value = "employee", required = false) String employee, + @RequestHeader(value = "HX-Request", required = false) String hxRequest, + Model model) { + + logger.info("Processing visitor registration for: {}", visitor.getEmail()); + + // Check for validation errors - Spring validation pattern + if (bindingResult.hasErrors()) { + logger.warn("Form validation failed with {} errors", bindingResult.getErrorCount()); + + // Add field errors and visitor to model for display in template + List fieldErrors = bindingResult.getFieldErrors(); + model.addAttribute("fieldErrors", fieldErrors); + model.addAttribute("visitor", visitor); + model.addAttribute("employees", employeeRepository.findAll()); // Add employees for dropdown + + if (hxRequest != null && hxRequest.equals("true")) { + return "fragments/validation-errors"; // HTMX error fragment + } + // Return the registration form template to show validation errors + return "visitorRegistration"; + } + resolveAndSetHost(visitor, host, employee); + Visit savedVisit = visitService.registerVisit(visitor); model.addAttribute("visitId", savedVisit.getId()); - - // If it's an HTMX request, just return the modal fragment + model.addAttribute("visit", savedVisit); + if (hxRequest != null && hxRequest.equals("true")) { - // JTE doesn't use Thymeleaf fragment syntax ("::"). Return the template name - // that corresponds to src/main/jte/fragments/otp-modal.jte return "fragments/otp-modal"; } - // For regular form submission (fallback) - return "otp-modal"; + // Regular form submission - return result view + return "visitorRegistrationResult"; + } + + @GetMapping("/report") + public String getReport(@RequestParam String period, Model model) { + List visits; + LocalDateTime now = LocalDateTime.now(); + + if (period.equals("daily")) { + visits = visitRepository.findAllByVisitDateBetween(now.toLocalDate().atStartOfDay(), now); + } else if (period.equals("monthly")) { + visits = visitRepository.findAllByVisitDateBetween(now.minusMonths(1), now); + } else { + return "error/400"; + } + model.addAttribute("visits", visits); + return "report"; } - // Updated to return Object so we can return ResponseEntity for HTMX redirects @PostMapping("/confirm-visit") public Object confirmVisit(@RequestParam("visitId") Long visitId, - @RequestParam("otpCode") String otpCode, - @RequestHeader(value = "HX-Request", required = false) String hxRequest, - Model model) { + @RequestParam("otpCode") String otpCode, + @RequestHeader(value = "HX-Request", required = false) String hxRequest, + Model model) { VerificationResult result = visitService.confirmVisit(visitId, otpCode); model.addAttribute("result", result); model.addAttribute("visitId", visitId); - // If it's an HTMX request, return a fragment or an HX-Redirect when attempts exhausted if (hxRequest != null && hxRequest.equals("true")) { if (result.success()) { - // Pass the visit to get visitor details for success message Visit visit = visitRepository.findById(visitId) - .orElseThrow(() -> new IllegalArgumentException("Visit not found")); + .orElseThrow(() -> new IllegalArgumentException("Visit not found")); model.addAttribute("visit", visit); - // Return the JTE template for success message return "fragments/success-message"; } else { - // If no more reattempts allowed, tell HTMX to redirect to the entry page if (!result.reattempt()) { return ResponseEntity.ok().header("HX-Redirect", "/").build(); } - // Auto-resend OTP when a failed attempt occurred and reattempts remain Visit visit = visitRepository.findById(visitId) - .orElseThrow(() -> new IllegalArgumentException("Visit not found")); + .orElseThrow(() -> new IllegalArgumentException("Visit not found")); - VerificationResult resendResult = otpService.generateOtp(visit, false); // don't reset attempt counter + VerificationResult resendResult = otpService.generateOtp(visit, false); - // Decide the message to show in the modal: prefer an explicit resend message when OTP re-sent successfully if (resendResult.success()) { model.addAttribute("serverMessage", "Invalid OTP. A new OTP has been sent to your email."); } else { - // If resend failed (cooldown or limit), show that message instead model.addAttribute("serverMessage", resendResult.message()); } - // Re-show the otp modal with an error message so HTMX swaps it in place return "fragments/otp-modal"; } } - - // For regular form submission (fallback): + if (result.success()) { return "confirmation-modal"; } else if (!result.reattempt()) { - // Attempts exhausted: redirect to blank visitor entry form return "redirect:/"; } else { - // Auto-resend for non-HTMX fallback as well Visit visit = visitRepository.findById(visitId) .orElseThrow(() -> new IllegalArgumentException("Visit not found")); @@ -184,14 +185,15 @@ public Object confirmVisit(@RequestParam("visitId") Long visitId, model.addAttribute("serverMessage", resendResult.message()); } - // Re-show otp page with message for non-HTMX fallback model.addAttribute("visitId", visitId); return "otp-modal"; } } @PostMapping("/resend-otp") - public String resendOtp(@RequestParam("visitId") Long visitId, Model model) { + public String resendOtp(@RequestParam("visitId") Long visitId, + @RequestHeader(value = "HX-Request", required = false) String hxRequest, + Model model) { Visit visit = visitRepository.findById(visitId) .orElseThrow(() -> new IllegalArgumentException("Visit not found")); @@ -200,8 +202,12 @@ public String resendOtp(@RequestParam("visitId") Long visitId, Model model) { model.addAttribute("result", result); model.addAttribute("visitId", visitId); model.addAttribute("serverMessage", result.message()); - // For HTMX flows this should probably return the otp modal again so the UI is updated. - return "fragments/otp-modal"; + + // Handle both HTMX and regular requests + if (hxRequest != null && hxRequest.equals("true")) { + return "fragments/otp-modal"; + } + return "otp-modal"; } /** @@ -226,4 +232,4 @@ private void resolveAndSetHost(Visitor visitor, String hostIdStr, String employe String trimmed = employeeStr.trim(); employeeRepository.findByNameIgnoreCase(trimmed).ifPresent(visitor::setHost); } -} +} \ No newline at end of file diff --git a/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java b/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java index f1abf22..cbc9e4f 100644 --- a/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java +++ b/web-backend/src/main/java/com/statusneo/vms/exception/GlobalExceptionHandler.java @@ -1,68 +1,130 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ package com.statusneo.vms.exception; import gg.jte.TemplateException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; import org.springframework.ui.Model; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolationException; import java.util.NoSuchElementException; +import java.util.stream.Collectors; + @ControllerAdvice public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String handleMethodArgumentNotValid(MethodArgumentNotValidException ex, Model model) { + String errorMessage = ex.getBindingResult() + .getFieldErrors() + .stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .collect(Collectors.joining(", ")); + + logger.warn("Method argument validation failed: {}", errorMessage); + model.addAttribute("error", "Validation failed: " + errorMessage); + model.addAttribute("fieldErrors", ex.getBindingResult().getFieldErrors()); + + return "error/400"; + } + + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String handleBindException(BindException ex, Model model) { + String errorMessage = ex.getFieldErrors() + .stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + + logger.warn("Binding error: {}", errorMessage); + model.addAttribute("error", "Form binding failed: " + errorMessage); + model.addAttribute("fieldErrors", ex.getFieldErrors()); + + return "error/400"; + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String handleMissingParams(MissingServletRequestParameterException ex, Model model) { + logger.warn("Missing required parameter: {}", ex.getParameterName()); + model.addAttribute("error", "Missing required parameter: " + ex.getParameterName()); + return "error/400"; + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String handleTypeMismatch(MethodArgumentTypeMismatchException ex, Model model) { + logger.warn("Type mismatch for parameter '{}': expected {}", + ex.getName(), ex.getRequiredType()); + model.addAttribute("error", + String.format("Invalid value for parameter '%s'. Expected type: %s", + ex.getName(), + ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "unknown")); + return "error/400"; + } + + @ExceptionHandler(NoSuchElementException.class) - public String handleNoSuchElement(NoSuchElementException ex, Model model) { - logger.warn("Resource not found", ex); - model.addAttribute("error", "The requested resource was not found."); - return "404"; + @ResponseStatus(HttpStatus.NOT_FOUND) + public String handleNoSuchElement(NoSuchElementException ex, HttpServletRequest request) { + logger.warn("Resource not found: {}", ex.getMessage()); + request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpStatus.NOT_FOUND.value()); + return "forward:/error/404.html"; } - @ExceptionHandler(DataIntegrityViolationException.class) - public String handleDataIntegrityViolation(DataIntegrityViolationException ex, Model model) { - logger.error("Data integrity violation", ex); - model.addAttribute("error", "A data validation error occurred. Please check your input."); + + @ExceptionHandler({DataIntegrityViolationException.class, IllegalArgumentException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String handleBadRequestExceptions(Exception ex, Model model) { + logger.warn("Bad request - {}: {}", ex.getClass().getSimpleName(), ex.getMessage()); + model.addAttribute("error", "Invalid request: " + ex.getMessage()); return "error/400"; } - @ExceptionHandler(IllegalArgumentException.class) - public String handleIllegalArgument(IllegalArgumentException ex, Model model) { - logger.warn("Invalid argument", ex); - model.addAttribute("error", "Invalid input provided."); + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String handleConstraintViolation(ConstraintViolationException ex, Model model) { + String errorMessage = ex.getConstraintViolations() + .stream() + .map(violation -> violation.getPropertyPath() + ": " + violation.getMessage()) + .collect(Collectors.joining(", ")); + + logger.warn("Constraint violation: {}", errorMessage); + model.addAttribute("error", "Invalid input: " + errorMessage); return "error/400"; } + @ExceptionHandler(TemplateException.class) - public String handleTemplateException(TemplateException ex, Model model) { - logger.error("Template rendering failed", ex); - model.addAttribute("error", "A technical error occurred while rendering the page."); - return "error/500"; + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public String handleTemplateException(TemplateException ex, HttpServletRequest request) { + logger.error("Template rendering failed: {}", ex.getMessage(), ex); + // NEVER return a JTE template here - use static HTML only + request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpStatus.INTERNAL_SERVER_ERROR.value()); + return "forward:/error/500.html"; } @ExceptionHandler(Exception.class) - public String handleGeneralException(Exception ex, Model model) { - logger.error("Unexpected error occurred", ex); - model.addAttribute("error", "Something went wrong. Please try again later."); - return "error/500"; + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public String handleGeneralException(Exception ex, HttpServletRequest request) { + logger.error("Unexpected error occurred - {}: {}", + ex.getClass().getSimpleName(), ex.getMessage(), ex); + // NEVER return a JTE template here - use static HTML only + request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpStatus.INTERNAL_SERVER_ERROR.value()); + return "forward:/error/500.html"; } -} +} \ No newline at end of file diff --git a/web-backend/src/main/java/com/statusneo/vms/model/Visitor.java b/web-backend/src/main/java/com/statusneo/vms/model/Visitor.java index 9e617bb..05dbed3 100644 --- a/web-backend/src/main/java/com/statusneo/vms/model/Visitor.java +++ b/web-backend/src/main/java/com/statusneo/vms/model/Visitor.java @@ -34,6 +34,8 @@ */ package com.statusneo.vms.model; +import java.time.LocalDateTime; + import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -43,11 +45,9 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.PrePersist; import jakarta.persistence.Table; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; -import java.time.LocalDateTime; - /** * Represents a visitor in the Visitor Management System. * This entity stores information about individuals visiting the premises. @@ -56,9 +56,6 @@ @Table(name = "visitor") public class Visitor { - private static final String PHONE_NUMBER_REGEX = "^\\d{10}$"; - - /** * Unique identifier for the visitor. */ @@ -69,29 +66,27 @@ public class Visitor { /** * Full name of the visitor. */ - private String name; + public String name; - /** - * Contact phone number of the visitor. - */ - @NotNull(message = "Phone number cannot be null") - @Pattern(regexp = "^\\d{10}$", message = "Phone number must be exactly 10 digits") - private String phoneNumber; + @Column(name = "phone_number") + @NotBlank(message = "Phone number is required") + @Pattern(regexp = "^[0-9]+$", message = "Phone number must contain only digits") + public String phoneNumber; /** * Email address of the visitor, used for communication and OTP verification. */ - private String email; + public String email; /** * Physical address of the visitor. */ - private String address; + public String address; /** * Company of the visitor. */ - private String company; + public String company; /** * Path to the visitor's profile picture stored in the system. @@ -99,9 +94,8 @@ public class Visitor { @Column(name = "picture_path") private String picturePath; - @Column(name = "laptop_number") - private String laptop; + public String laptop; @ManyToOne @JoinColumn(name = "host_id", referencedColumnName = "id") @@ -125,11 +119,11 @@ public Visitor() { /** * Constructs a new Visitor with the specified details. * - * @param id The unique identifier - * @param name The visitor's full name + * @param id The unique identifier + * @param name The visitor's full name * @param phoneNumber The visitor's phone number - * @param email The visitor's email address - * @param address The visitor's physical address + * @param email The visitor's email address + * @param address The visitor's physical address */ public Visitor(Long id, String name, String phoneNumber, String email, String address) { this.id = id; @@ -154,7 +148,7 @@ protected void onCreate() { this.createdAt = LocalDateTime.now(); } - public LocalDateTime getCreatedAt(){ + public LocalDateTime getCreatedAt() { return createdAt; } @@ -181,6 +175,7 @@ public String getPhoneNumber() { public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } + public String getEmail() { return email; } @@ -208,6 +203,7 @@ public void setCompany(String company) { public String getPicturePath() { return picturePath; } + public void setPicturePath(String picturePath) { this.picturePath = picturePath; } @@ -220,7 +216,6 @@ public void setLaptop(String laptop) { this.laptop = laptop; } - @Override public String toString() { return "Visitor{" + diff --git a/web-backend/src/main/jte/error/400.jte b/web-backend/src/main/jte/error/400.jte index f922a54..4cea066 100644 --- a/web-backend/src/main/jte/error/400.jte +++ b/web-backend/src/main/jte/error/400.jte @@ -1,8 +1,94 @@ - -Bad Request +@import org.springframework.validation.FieldError +@import java.util.List + +@param String error = "Bad Request" +@param List fieldErrors = null + + + + + + + 400 - Bad Request + + -

Invalid Request

-

@error

-Back to Home +
+

400 - Bad Request

+ +
+ ${error} +
+ + @if(fieldErrors != null && !fieldErrors.isEmpty()) +

Validation Errors:

+
    + @for(FieldError fieldError : fieldErrors) +
  • + ${fieldError.getField()}: + ${fieldError.getDefaultMessage()} +
  • + @endfor +
+ @endif + + Return to Home +
\ No newline at end of file diff --git a/web-backend/src/main/jte/error/404.jte b/web-backend/src/main/jte/error/404.jte deleted file mode 100644 index f51a3e6..0000000 --- a/web-backend/src/main/jte/error/404.jte +++ /dev/null @@ -1,8 +0,0 @@ - -404 Not Found - -

Oops! Page Not Found

-

@error

-Back to Home - - \ No newline at end of file diff --git a/web-backend/src/main/jte/error/500.jte b/web-backend/src/main/jte/error/500.jte deleted file mode 100644 index ead84d9..0000000 --- a/web-backend/src/main/jte/error/500.jte +++ /dev/null @@ -1,8 +0,0 @@ - -Server Error - -

Internal Server Error

-

@error

-Back to Home - - \ No newline at end of file diff --git a/web-backend/src/main/jte/index.jte b/web-backend/src/main/jte/index.jte index f99b330..6418a89 100644 --- a/web-backend/src/main/jte/index.jte +++ b/web-backend/src/main/jte/index.jte @@ -1,3 +1,10 @@ +@import org.springframework.validation.FieldError +@import java.util.List +@import com.statusneo.vms.model.Visitor + +@param Visitor visitor = new com.statusneo.vms.model.Visitor() +@param List fieldErrors = null + @@ -24,8 +31,25 @@
- - + + + @if(fieldErrors != null && !fieldErrors.isEmpty()) +
+
+ + + +
+

Please correct the following errors:

+
    + @for(FieldError error : fieldErrors) +
  • ${error.getField()}: ${error.getDefaultMessage()}
  • + @endfor +
+
+
+
+ @endif
@@ -64,8 +89,9 @@ @@ -79,9 +105,27 @@ + pattern="^\d{10}$" + class="w-full px-4 py-3 border ${fieldErrors != null && fieldErrors.stream().anyMatch(e -> e.getField().equals("phoneNumber")) ? "border-red-500" : "border-gray-300"} rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" + placeholder="Enter your 10-digit phone number" + oninput="validatePhoneNumber(this)" + onkeypress="return isNumberKey(event)"> + + + + + + @if(fieldErrors != null) + @for(FieldError error : fieldErrors) + @if(error.getField().equals("phoneNumber")) +

${error.getDefaultMessage()}

+ @endif + @endfor + @endif
@@ -94,8 +138,9 @@
@@ -109,6 +154,7 @@ @@ -125,7 +171,7 @@ id="host-search" name="hostSearch" autocomplete="off" - hx-get="/search-employees" + hx-get="/api/hosts/search" hx-trigger="keyup changed delay:300ms" hx-target="#host-results" hx-indicator="#search-spinner" @@ -135,7 +181,8 @@ -
+ +
- - + - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file