mirror of
				https://github.com/9technologygroup/patchmon.net.git
				synced 2025-11-04 05:53:27 +00:00 
			
		
		
		
	fix(frontend): A form label must be associated with an input
This commit is contained in:
		@@ -31,7 +31,7 @@ import {
 | 
			
		||||
	Wifi,
 | 
			
		||||
	X,
 | 
			
		||||
} from "lucide-react";
 | 
			
		||||
import React, { useState } from "react";
 | 
			
		||||
import React, { useId, useState } from "react";
 | 
			
		||||
import { Link, useNavigate, useParams } from "react-router-dom";
 | 
			
		||||
import InlineEdit from "../components/InlineEdit";
 | 
			
		||||
import {
 | 
			
		||||
@@ -1001,6 +1001,8 @@ const HostDetail = () => {
 | 
			
		||||
const CredentialsModal = ({ host, isOpen, onClose }) => {
 | 
			
		||||
	const [showApiKey, setShowApiKey] = useState(false);
 | 
			
		||||
	const [activeTab, setActiveTab] = useState("quick-install");
 | 
			
		||||
	const apiIdInputId = useId();
 | 
			
		||||
	const apiKeyInputId = useId();
 | 
			
		||||
 | 
			
		||||
	const { data: serverUrlData } = useQuery({
 | 
			
		||||
		queryKey: ["serverUrl"],
 | 
			
		||||
@@ -1342,11 +1344,15 @@ echo "   - View logs: tail -f /var/log/patchmon-agent.log"`;
 | 
			
		||||
							</h4>
 | 
			
		||||
							<div className="space-y-4">
 | 
			
		||||
								<div>
 | 
			
		||||
									<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
									<label
 | 
			
		||||
										htmlFor={apiIdInputId}
 | 
			
		||||
										className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
									>
 | 
			
		||||
										API ID
 | 
			
		||||
									</label>
 | 
			
		||||
									<div className="flex items-center gap-2">
 | 
			
		||||
										<input
 | 
			
		||||
											id={apiIdInputId}
 | 
			
		||||
											type="text"
 | 
			
		||||
											value={host.api_id}
 | 
			
		||||
											readOnly
 | 
			
		||||
@@ -1364,11 +1370,15 @@ echo "   - View logs: tail -f /var/log/patchmon-agent.log"`;
 | 
			
		||||
								</div>
 | 
			
		||||
 | 
			
		||||
								<div>
 | 
			
		||||
									<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
									<label
 | 
			
		||||
										htmlFor={apiKeyInputId}
 | 
			
		||||
										className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
									>
 | 
			
		||||
										API Key
 | 
			
		||||
									</label>
 | 
			
		||||
									<div className="flex items-center gap-2">
 | 
			
		||||
										<input
 | 
			
		||||
											id={apiKeyInputId}
 | 
			
		||||
											type={showApiKey ? "text" : "password"}
 | 
			
		||||
											value={host.api_key}
 | 
			
		||||
											readOnly
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import {
 | 
			
		||||
	Trash2,
 | 
			
		||||
	Users,
 | 
			
		||||
} from "lucide-react";
 | 
			
		||||
import React, { useState } from "react";
 | 
			
		||||
import React, { useId, useState } from "react";
 | 
			
		||||
import { hostGroupsAPI } from "../utils/api";
 | 
			
		||||
 | 
			
		||||
const HostGroups = () => {
 | 
			
		||||
@@ -252,6 +252,9 @@ const HostGroups = () => {
 | 
			
		||||
 | 
			
		||||
// Create Host Group Modal
 | 
			
		||||
const CreateHostGroupModal = ({ onClose, onSubmit, isLoading }) => {
 | 
			
		||||
	const nameId = useId();
 | 
			
		||||
	const descriptionId = useId();
 | 
			
		||||
	const colorId = useId();
 | 
			
		||||
	const [formData, setFormData] = useState({
 | 
			
		||||
		name: "",
 | 
			
		||||
		description: "",
 | 
			
		||||
@@ -279,11 +282,15 @@ const CreateHostGroupModal = ({ onClose, onSubmit, isLoading }) => {
 | 
			
		||||
 | 
			
		||||
				<form onSubmit={handleSubmit} className="space-y-4">
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={nameId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Name *
 | 
			
		||||
						</label>
 | 
			
		||||
						<input
 | 
			
		||||
							type="text"
 | 
			
		||||
							id={nameId}
 | 
			
		||||
							name="name"
 | 
			
		||||
							value={formData.name}
 | 
			
		||||
							onChange={handleChange}
 | 
			
		||||
@@ -294,10 +301,14 @@ const CreateHostGroupModal = ({ onClose, onSubmit, isLoading }) => {
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={descriptionId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Description
 | 
			
		||||
						</label>
 | 
			
		||||
						<textarea
 | 
			
		||||
							id={descriptionId}
 | 
			
		||||
							name="description"
 | 
			
		||||
							value={formData.description}
 | 
			
		||||
							onChange={handleChange}
 | 
			
		||||
@@ -308,12 +319,16 @@ const CreateHostGroupModal = ({ onClose, onSubmit, isLoading }) => {
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={colorId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Color
 | 
			
		||||
						</label>
 | 
			
		||||
						<div className="flex items-center gap-3">
 | 
			
		||||
							<input
 | 
			
		||||
								type="color"
 | 
			
		||||
								id={colorId}
 | 
			
		||||
								name="color"
 | 
			
		||||
								value={formData.color}
 | 
			
		||||
								onChange={handleChange}
 | 
			
		||||
@@ -350,6 +365,9 @@ const CreateHostGroupModal = ({ onClose, onSubmit, isLoading }) => {
 | 
			
		||||
 | 
			
		||||
// Edit Host Group Modal
 | 
			
		||||
const EditHostGroupModal = ({ group, onClose, onSubmit, isLoading }) => {
 | 
			
		||||
	const editNameId = useId();
 | 
			
		||||
	const editDescriptionId = useId();
 | 
			
		||||
	const editColorId = useId();
 | 
			
		||||
	const [formData, setFormData] = useState({
 | 
			
		||||
		name: group.name,
 | 
			
		||||
		description: group.description || "",
 | 
			
		||||
@@ -377,11 +395,15 @@ const EditHostGroupModal = ({ group, onClose, onSubmit, isLoading }) => {
 | 
			
		||||
 | 
			
		||||
				<form onSubmit={handleSubmit} className="space-y-4">
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={editNameId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Name *
 | 
			
		||||
						</label>
 | 
			
		||||
						<input
 | 
			
		||||
							type="text"
 | 
			
		||||
							id={editNameId}
 | 
			
		||||
							name="name"
 | 
			
		||||
							value={formData.name}
 | 
			
		||||
							onChange={handleChange}
 | 
			
		||||
@@ -392,10 +414,14 @@ const EditHostGroupModal = ({ group, onClose, onSubmit, isLoading }) => {
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={editDescriptionId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Description
 | 
			
		||||
						</label>
 | 
			
		||||
						<textarea
 | 
			
		||||
							id={editDescriptionId}
 | 
			
		||||
							name="description"
 | 
			
		||||
							value={formData.description}
 | 
			
		||||
							onChange={handleChange}
 | 
			
		||||
@@ -406,12 +432,16 @@ const EditHostGroupModal = ({ group, onClose, onSubmit, isLoading }) => {
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={editColorId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Color
 | 
			
		||||
						</label>
 | 
			
		||||
						<div className="flex items-center gap-3">
 | 
			
		||||
							<input
 | 
			
		||||
								type="color"
 | 
			
		||||
								id={editColorId}
 | 
			
		||||
								name="color"
 | 
			
		||||
								value={formData.color}
 | 
			
		||||
								onChange={handleChange}
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ import {
 | 
			
		||||
	Users,
 | 
			
		||||
	X,
 | 
			
		||||
} from "lucide-react";
 | 
			
		||||
import React, { useEffect, useState } from "react";
 | 
			
		||||
import React, { useEffect, useId, useState } from "react";
 | 
			
		||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
 | 
			
		||||
import InlineEdit from "../components/InlineEdit";
 | 
			
		||||
import InlineGroupEdit from "../components/InlineGroupEdit";
 | 
			
		||||
@@ -43,6 +43,8 @@ import { OSIcon } from "../utils/osIcons.jsx";
 | 
			
		||||
 | 
			
		||||
// Add Host Modal Component
 | 
			
		||||
const AddHostModal = ({ isOpen, onClose, onSuccess }) => {
 | 
			
		||||
	const friendlyNameId = useId();
 | 
			
		||||
	const hostGroupId = useId();
 | 
			
		||||
	const [formData, setFormData] = useState({
 | 
			
		||||
		friendly_name: "",
 | 
			
		||||
		hostGroupId: "",
 | 
			
		||||
@@ -113,11 +115,15 @@ const AddHostModal = ({ isOpen, onClose, onSuccess }) => {
 | 
			
		||||
 | 
			
		||||
				<form onSubmit={handleSubmit} className="space-y-6">
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={friendlyNameId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
 | 
			
		||||
						>
 | 
			
		||||
							Friendly Name *
 | 
			
		||||
						</label>
 | 
			
		||||
						<input
 | 
			
		||||
							type="text"
 | 
			
		||||
							id={friendlyNameId}
 | 
			
		||||
							required
 | 
			
		||||
							value={formData.friendly_name}
 | 
			
		||||
							onChange={(e) =>
 | 
			
		||||
@@ -133,9 +139,9 @@ const AddHostModal = ({ isOpen, onClose, onSuccess }) => {
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-3">
 | 
			
		||||
						<span className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-3">
 | 
			
		||||
							Host Group
 | 
			
		||||
						</label>
 | 
			
		||||
						</span>
 | 
			
		||||
						<div className="grid grid-cols-3 gap-2">
 | 
			
		||||
							{/* No Group Option */}
 | 
			
		||||
							<button
 | 
			
		||||
@@ -231,6 +237,8 @@ const AddHostModal = ({ isOpen, onClose, onSuccess }) => {
 | 
			
		||||
 | 
			
		||||
// Credentials Modal Component
 | 
			
		||||
const CredentialsModal = ({ host, isOpen, onClose }) => {
 | 
			
		||||
	const apiIdId = useId();
 | 
			
		||||
	const apiKeyId = useId();
 | 
			
		||||
	const [showApiKey, setShowApiKey] = useState(false);
 | 
			
		||||
	const [activeTab, setActiveTab] = useState(
 | 
			
		||||
		host?.isNewHost ? "quick" : "credentials",
 | 
			
		||||
@@ -508,12 +516,16 @@ echo "   - View logs: tail -f /var/log/patchmon-agent.log"`,
 | 
			
		||||
				{activeTab === "credentials" && (
 | 
			
		||||
					<div className="space-y-4">
 | 
			
		||||
						<div>
 | 
			
		||||
							<label className="block text-sm font-medium text-secondary-700 mb-2">
 | 
			
		||||
							<label
 | 
			
		||||
								htmlFor={apiIdId}
 | 
			
		||||
								className="block text-sm font-medium text-secondary-700 mb-2"
 | 
			
		||||
							>
 | 
			
		||||
								API ID
 | 
			
		||||
							</label>
 | 
			
		||||
							<div className="flex">
 | 
			
		||||
								<input
 | 
			
		||||
									type="text"
 | 
			
		||||
									id={apiIdId}
 | 
			
		||||
									readOnly
 | 
			
		||||
									value={host.apiId}
 | 
			
		||||
									className="flex-1 block w-full border-secondary-300 rounded-l-md shadow-sm bg-secondary-50"
 | 
			
		||||
@@ -529,12 +541,16 @@ echo "   - View logs: tail -f /var/log/patchmon-agent.log"`,
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
						<div>
 | 
			
		||||
							<label className="block text-sm font-medium text-secondary-700 mb-2">
 | 
			
		||||
							<label
 | 
			
		||||
								htmlFor={apiKeyId}
 | 
			
		||||
								className="block text-sm font-medium text-secondary-700 mb-2"
 | 
			
		||||
							>
 | 
			
		||||
								API Key
 | 
			
		||||
							</label>
 | 
			
		||||
							<div className="flex">
 | 
			
		||||
								<input
 | 
			
		||||
									type={showApiKey ? "text" : "password"}
 | 
			
		||||
									id={apiKeyId}
 | 
			
		||||
									readOnly
 | 
			
		||||
									value={host.apiKey}
 | 
			
		||||
									className="flex-1 block w-full border-secondary-300 rounded-l-md shadow-sm bg-secondary-50"
 | 
			
		||||
@@ -779,6 +795,10 @@ echo "   - View logs: tail -f /var/log/patchmon-agent.log"`,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Hosts = () => {
 | 
			
		||||
	const hostGroupFilterId = useId();
 | 
			
		||||
	const statusFilterId = useId();
 | 
			
		||||
	const osFilterId = useId();
 | 
			
		||||
	const bulkHostGroupId = useId();
 | 
			
		||||
	const [showAddModal, setShowAddModal] = useState(false);
 | 
			
		||||
	const [selectedHosts, setSelectedHosts] = useState([]);
 | 
			
		||||
	const [showBulkAssignModal, setShowBulkAssignModal] = useState(false);
 | 
			
		||||
@@ -1737,10 +1757,14 @@ const Hosts = () => {
 | 
			
		||||
							<div className="bg-secondary-50 dark:bg-secondary-700 p-4 rounded-lg border dark:border-secondary-600">
 | 
			
		||||
								<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
 | 
			
		||||
									<div>
 | 
			
		||||
										<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
										<label
 | 
			
		||||
											htmlFor={hostGroupFilterId}
 | 
			
		||||
											className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
										>
 | 
			
		||||
											Host Group
 | 
			
		||||
										</label>
 | 
			
		||||
										<select
 | 
			
		||||
											id={hostGroupFilterId}
 | 
			
		||||
											value={groupFilter}
 | 
			
		||||
											onChange={(e) => setGroupFilter(e.target.value)}
 | 
			
		||||
											className="w-full border border-secondary-300 dark:border-secondary-600 rounded-lg px-3 py-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white"
 | 
			
		||||
@@ -1755,10 +1779,14 @@ const Hosts = () => {
 | 
			
		||||
										</select>
 | 
			
		||||
									</div>
 | 
			
		||||
									<div>
 | 
			
		||||
										<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
										<label
 | 
			
		||||
											htmlFor={statusFilterId}
 | 
			
		||||
											className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
										>
 | 
			
		||||
											Status
 | 
			
		||||
										</label>
 | 
			
		||||
										<select
 | 
			
		||||
											id={statusFilterId}
 | 
			
		||||
											value={statusFilter}
 | 
			
		||||
											onChange={(e) => setStatusFilter(e.target.value)}
 | 
			
		||||
											className="w-full border border-secondary-300 dark:border-secondary-600 rounded-lg px-3 py-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white"
 | 
			
		||||
@@ -1771,10 +1799,14 @@ const Hosts = () => {
 | 
			
		||||
										</select>
 | 
			
		||||
									</div>
 | 
			
		||||
									<div>
 | 
			
		||||
										<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
										<label
 | 
			
		||||
											htmlFor={osFilterId}
 | 
			
		||||
											className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
										>
 | 
			
		||||
											Operating System
 | 
			
		||||
										</label>
 | 
			
		||||
										<select
 | 
			
		||||
											id={osFilterId}
 | 
			
		||||
											value={osFilter}
 | 
			
		||||
											onChange={(e) => setOsFilter(e.target.value)}
 | 
			
		||||
											className="w-full border border-secondary-300 dark:border-secondary-600 rounded-lg px-3 py-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white"
 | 
			
		||||
@@ -2116,10 +2148,14 @@ const BulkAssignModal = ({
 | 
			
		||||
 | 
			
		||||
				<form onSubmit={handleSubmit} className="space-y-4">
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={bulkHostGroupId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Host Group
 | 
			
		||||
						</label>
 | 
			
		||||
						<select
 | 
			
		||||
							id={bulkHostGroupId}
 | 
			
		||||
							value={selectedGroupId}
 | 
			
		||||
							onChange={(e) => setSelectedGroupId(e.target.value)}
 | 
			
		||||
							className="w-full px-3 py-2 border border-secondary-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500"
 | 
			
		||||
@@ -2309,7 +2345,7 @@ const ColumnSettingsModal = ({
 | 
			
		||||
								onDragOver={handleDragOver}
 | 
			
		||||
								onDrop={(e) => handleDrop(e, index)}
 | 
			
		||||
								onKeyDown={(e) => {
 | 
			
		||||
									if (e.key === 'Enter' || e.key === ' ') {
 | 
			
		||||
									if (e.key === "Enter" || e.key === " ") {
 | 
			
		||||
										e.preventDefault();
 | 
			
		||||
										// Focus handling for keyboard users
 | 
			
		||||
									}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import {
 | 
			
		||||
	Trash2,
 | 
			
		||||
	Users,
 | 
			
		||||
} from "lucide-react";
 | 
			
		||||
import React, { useState } from "react";
 | 
			
		||||
import React, { useId, useState } from "react";
 | 
			
		||||
import { hostGroupsAPI } from "../utils/api";
 | 
			
		||||
 | 
			
		||||
const Options = () => {
 | 
			
		||||
@@ -332,6 +332,9 @@ const Options = () => {
 | 
			
		||||
 | 
			
		||||
// Create Host Group Modal
 | 
			
		||||
const CreateHostGroupModal = ({ onClose, onSubmit, isLoading }) => {
 | 
			
		||||
	const nameId = useId();
 | 
			
		||||
	const descriptionId = useId();
 | 
			
		||||
	const colorId = useId();
 | 
			
		||||
	const [formData, setFormData] = useState({
 | 
			
		||||
		name: "",
 | 
			
		||||
		description: "",
 | 
			
		||||
@@ -359,11 +362,15 @@ const CreateHostGroupModal = ({ onClose, onSubmit, isLoading }) => {
 | 
			
		||||
 | 
			
		||||
				<form onSubmit={handleSubmit} className="space-y-4">
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={nameId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Name *
 | 
			
		||||
						</label>
 | 
			
		||||
						<input
 | 
			
		||||
							type="text"
 | 
			
		||||
							id={nameId}
 | 
			
		||||
							name="name"
 | 
			
		||||
							value={formData.name}
 | 
			
		||||
							onChange={handleChange}
 | 
			
		||||
@@ -374,10 +381,14 @@ const CreateHostGroupModal = ({ onClose, onSubmit, isLoading }) => {
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={descriptionId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Description
 | 
			
		||||
						</label>
 | 
			
		||||
						<textarea
 | 
			
		||||
							id={descriptionId}
 | 
			
		||||
							name="description"
 | 
			
		||||
							value={formData.description}
 | 
			
		||||
							onChange={handleChange}
 | 
			
		||||
@@ -388,12 +399,16 @@ const CreateHostGroupModal = ({ onClose, onSubmit, isLoading }) => {
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={colorId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Color
 | 
			
		||||
						</label>
 | 
			
		||||
						<div className="flex items-center gap-3">
 | 
			
		||||
							<input
 | 
			
		||||
								type="color"
 | 
			
		||||
								id={colorId}
 | 
			
		||||
								name="color"
 | 
			
		||||
								value={formData.color}
 | 
			
		||||
								onChange={handleChange}
 | 
			
		||||
@@ -430,6 +445,9 @@ const CreateHostGroupModal = ({ onClose, onSubmit, isLoading }) => {
 | 
			
		||||
 | 
			
		||||
// Edit Host Group Modal
 | 
			
		||||
const EditHostGroupModal = ({ group, onClose, onSubmit, isLoading }) => {
 | 
			
		||||
	const editNameId = useId();
 | 
			
		||||
	const editDescriptionId = useId();
 | 
			
		||||
	const editColorId = useId();
 | 
			
		||||
	const [formData, setFormData] = useState({
 | 
			
		||||
		name: group.name,
 | 
			
		||||
		description: group.description || "",
 | 
			
		||||
@@ -457,11 +475,15 @@ const EditHostGroupModal = ({ group, onClose, onSubmit, isLoading }) => {
 | 
			
		||||
 | 
			
		||||
				<form onSubmit={handleSubmit} className="space-y-4">
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={editNameId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Name *
 | 
			
		||||
						</label>
 | 
			
		||||
						<input
 | 
			
		||||
							type="text"
 | 
			
		||||
							id={editNameId}
 | 
			
		||||
							name="name"
 | 
			
		||||
							value={formData.name}
 | 
			
		||||
							onChange={handleChange}
 | 
			
		||||
@@ -472,10 +494,14 @@ const EditHostGroupModal = ({ group, onClose, onSubmit, isLoading }) => {
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={editDescriptionId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Description
 | 
			
		||||
						</label>
 | 
			
		||||
						<textarea
 | 
			
		||||
							id={editDescriptionId}
 | 
			
		||||
							name="description"
 | 
			
		||||
							value={formData.description}
 | 
			
		||||
							onChange={handleChange}
 | 
			
		||||
@@ -486,12 +512,16 @@ const EditHostGroupModal = ({ group, onClose, onSubmit, isLoading }) => {
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={editColorId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Color
 | 
			
		||||
						</label>
 | 
			
		||||
						<div className="flex items-center gap-3">
 | 
			
		||||
							<input
 | 
			
		||||
								type="color"
 | 
			
		||||
								id={editColorId}
 | 
			
		||||
								name="color"
 | 
			
		||||
								value={formData.color}
 | 
			
		||||
								onChange={handleChange}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ import {
 | 
			
		||||
	Users,
 | 
			
		||||
	X,
 | 
			
		||||
} from "lucide-react";
 | 
			
		||||
import React, { useEffect, useState } from "react";
 | 
			
		||||
import React, { useEffect, useId, useState } from "react";
 | 
			
		||||
import { useAuth } from "../contexts/AuthContext";
 | 
			
		||||
import { permissionsAPI } from "../utils/api";
 | 
			
		||||
 | 
			
		||||
@@ -320,6 +320,7 @@ const RolePermissionsCard = ({
 | 
			
		||||
							<div key={field.key} className="flex items-start">
 | 
			
		||||
								<div className="flex items-center h-5">
 | 
			
		||||
									<input
 | 
			
		||||
										id={`${role.role}-${field.key}`}
 | 
			
		||||
										type="checkbox"
 | 
			
		||||
										checked={isChecked}
 | 
			
		||||
										onChange={(e) =>
 | 
			
		||||
@@ -335,7 +336,10 @@ const RolePermissionsCard = ({
 | 
			
		||||
								<div className="ml-3">
 | 
			
		||||
									<div className="flex items-center">
 | 
			
		||||
										<Icon className="h-4 w-4 text-secondary-400 mr-2" />
 | 
			
		||||
										<label className="text-sm font-medium text-secondary-900 dark:text-white">
 | 
			
		||||
										<label
 | 
			
		||||
											htmlFor={`${role.role}-${field.key}`}
 | 
			
		||||
											className="text-sm font-medium text-secondary-900 dark:text-white"
 | 
			
		||||
										>
 | 
			
		||||
											{field.label}
 | 
			
		||||
										</label>
 | 
			
		||||
									</div>
 | 
			
		||||
@@ -354,6 +358,7 @@ const RolePermissionsCard = ({
 | 
			
		||||
 | 
			
		||||
// Add Role Modal Component
 | 
			
		||||
const AddRoleModal = ({ isOpen, onClose, onSuccess }) => {
 | 
			
		||||
	const roleNameInputId = useId();
 | 
			
		||||
	const [formData, setFormData] = useState({
 | 
			
		||||
		role: "",
 | 
			
		||||
		can_view_dashboard: true,
 | 
			
		||||
@@ -404,10 +409,14 @@ const AddRoleModal = ({ isOpen, onClose, onSuccess }) => {
 | 
			
		||||
 | 
			
		||||
				<form onSubmit={handleSubmit} className="space-y-4">
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={roleNameInputId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Role Name
 | 
			
		||||
						</label>
 | 
			
		||||
						<input
 | 
			
		||||
							id={roleNameInputId}
 | 
			
		||||
							type="text"
 | 
			
		||||
							name="role"
 | 
			
		||||
							required
 | 
			
		||||
@@ -439,13 +448,17 @@ const AddRoleModal = ({ isOpen, onClose, onSuccess }) => {
 | 
			
		||||
						].map((permission) => (
 | 
			
		||||
							<div key={permission.key} className="flex items-center">
 | 
			
		||||
								<input
 | 
			
		||||
									id={`add-role-${permission.key}`}
 | 
			
		||||
									type="checkbox"
 | 
			
		||||
									name={permission.key}
 | 
			
		||||
									checked={formData[permission.key]}
 | 
			
		||||
									onChange={handleInputChange}
 | 
			
		||||
									className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded"
 | 
			
		||||
								/>
 | 
			
		||||
								<label className="ml-2 block text-sm text-secondary-700 dark:text-secondary-200">
 | 
			
		||||
								<label
 | 
			
		||||
									htmlFor={`add-role-${permission.key}`}
 | 
			
		||||
									className="ml-2 block text-sm text-secondary-700 dark:text-secondary-200"
 | 
			
		||||
								>
 | 
			
		||||
									{permission.label}
 | 
			
		||||
								</label>
 | 
			
		||||
							</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,8 @@ const Profile = () => {
 | 
			
		||||
	const currentPasswordId = useId();
 | 
			
		||||
	const newPasswordId = useId();
 | 
			
		||||
	const confirmPasswordId = useId();
 | 
			
		||||
	const verificationTokenId = useId();
 | 
			
		||||
	const disablePasswordId = useId();
 | 
			
		||||
	const { user, updateProfile, changePassword } = useAuth();
 | 
			
		||||
	const { theme, toggleTheme, isDark } = useTheme();
 | 
			
		||||
	const [activeTab, setActiveTab] = useState("profile");
 | 
			
		||||
@@ -924,10 +926,14 @@ const TfaTab = () => {
 | 
			
		||||
						</p>
 | 
			
		||||
						<form onSubmit={handleVerify} className="space-y-4">
 | 
			
		||||
							<div>
 | 
			
		||||
								<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
								<label
 | 
			
		||||
									htmlFor={verificationTokenId}
 | 
			
		||||
									className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
								>
 | 
			
		||||
									Verification Code
 | 
			
		||||
								</label>
 | 
			
		||||
								<input
 | 
			
		||||
									id={verificationTokenId}
 | 
			
		||||
									type="text"
 | 
			
		||||
									value={verificationToken}
 | 
			
		||||
									onChange={(e) =>
 | 
			
		||||
@@ -1030,10 +1036,14 @@ const TfaTab = () => {
 | 
			
		||||
						</p>
 | 
			
		||||
						<form onSubmit={handleDisable} className="space-y-4">
 | 
			
		||||
							<div>
 | 
			
		||||
								<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
								<label
 | 
			
		||||
									htmlFor={disablePasswordId}
 | 
			
		||||
									className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
								>
 | 
			
		||||
									Password
 | 
			
		||||
								</label>
 | 
			
		||||
								<input
 | 
			
		||||
									id={disablePasswordId}
 | 
			
		||||
									type="password"
 | 
			
		||||
									value={password}
 | 
			
		||||
									onChange={(e) => setPassword(e.target.value)}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,9 @@ import { repositoryAPI } from "../utils/api";
 | 
			
		||||
 | 
			
		||||
const RepositoryDetail = () => {
 | 
			
		||||
	const isActiveId = useId();
 | 
			
		||||
	const repositoryNameId = useId();
 | 
			
		||||
	const priorityId = useId();
 | 
			
		||||
	const descriptionId = useId();
 | 
			
		||||
	const { repositoryId } = useParams();
 | 
			
		||||
	const queryClient = useQueryClient();
 | 
			
		||||
	const [editMode, setEditMode] = useState(false);
 | 
			
		||||
@@ -201,11 +204,15 @@ const RepositoryDetail = () => {
 | 
			
		||||
					{editMode ? (
 | 
			
		||||
						<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
 | 
			
		||||
							<div>
 | 
			
		||||
								<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
 | 
			
		||||
								<label
 | 
			
		||||
									htmlFor={repositoryNameId}
 | 
			
		||||
									className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1"
 | 
			
		||||
								>
 | 
			
		||||
									Repository Name
 | 
			
		||||
								</label>
 | 
			
		||||
								<input
 | 
			
		||||
									type="text"
 | 
			
		||||
									id={repositoryNameId}
 | 
			
		||||
									value={formData.name}
 | 
			
		||||
									onChange={(e) =>
 | 
			
		||||
										setFormData({ ...formData, name: e.target.value })
 | 
			
		||||
@@ -214,11 +221,15 @@ const RepositoryDetail = () => {
 | 
			
		||||
								/>
 | 
			
		||||
							</div>
 | 
			
		||||
							<div>
 | 
			
		||||
								<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
 | 
			
		||||
								<label
 | 
			
		||||
									htmlFor={priorityId}
 | 
			
		||||
									className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1"
 | 
			
		||||
								>
 | 
			
		||||
									Priority
 | 
			
		||||
								</label>
 | 
			
		||||
								<input
 | 
			
		||||
									type="number"
 | 
			
		||||
									id={priorityId}
 | 
			
		||||
									value={formData.priority}
 | 
			
		||||
									onChange={(e) =>
 | 
			
		||||
										setFormData({ ...formData, priority: e.target.value })
 | 
			
		||||
@@ -228,10 +239,14 @@ const RepositoryDetail = () => {
 | 
			
		||||
								/>
 | 
			
		||||
							</div>
 | 
			
		||||
							<div className="md:col-span-2">
 | 
			
		||||
								<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
 | 
			
		||||
								<label
 | 
			
		||||
									htmlFor={descriptionId}
 | 
			
		||||
									className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1"
 | 
			
		||||
								>
 | 
			
		||||
									Description
 | 
			
		||||
								</label>
 | 
			
		||||
								<textarea
 | 
			
		||||
									id={descriptionId}
 | 
			
		||||
									value={formData.description}
 | 
			
		||||
									onChange={(e) =>
 | 
			
		||||
										setFormData({ ...formData, description: e.target.value })
 | 
			
		||||
@@ -263,9 +278,9 @@ const RepositoryDetail = () => {
 | 
			
		||||
						<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
 | 
			
		||||
							<div className="space-y-4">
 | 
			
		||||
								<div>
 | 
			
		||||
									<label className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
									<span className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
										URL
 | 
			
		||||
									</label>
 | 
			
		||||
									</span>
 | 
			
		||||
									<div className="flex items-center mt-1">
 | 
			
		||||
										<Globe className="h-4 w-4 text-secondary-400 mr-2" />
 | 
			
		||||
										<span className="text-secondary-900 dark:text-white">
 | 
			
		||||
@@ -274,25 +289,25 @@ const RepositoryDetail = () => {
 | 
			
		||||
									</div>
 | 
			
		||||
								</div>
 | 
			
		||||
								<div>
 | 
			
		||||
									<label className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
									<span className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
										Distribution
 | 
			
		||||
									</label>
 | 
			
		||||
									</span>
 | 
			
		||||
									<p className="text-secondary-900 dark:text-white mt-1">
 | 
			
		||||
										{repository.distribution}
 | 
			
		||||
									</p>
 | 
			
		||||
								</div>
 | 
			
		||||
								<div>
 | 
			
		||||
									<label className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
									<span className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
										Components
 | 
			
		||||
									</label>
 | 
			
		||||
									</span>
 | 
			
		||||
									<p className="text-secondary-900 dark:text-white mt-1">
 | 
			
		||||
										{repository.components}
 | 
			
		||||
									</p>
 | 
			
		||||
								</div>
 | 
			
		||||
								<div>
 | 
			
		||||
									<label className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
									<span className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
										Repository Type
 | 
			
		||||
									</label>
 | 
			
		||||
									</span>
 | 
			
		||||
									<p className="text-secondary-900 dark:text-white mt-1">
 | 
			
		||||
										{repository.repoType}
 | 
			
		||||
									</p>
 | 
			
		||||
@@ -300,9 +315,9 @@ const RepositoryDetail = () => {
 | 
			
		||||
							</div>
 | 
			
		||||
							<div className="space-y-4">
 | 
			
		||||
								<div>
 | 
			
		||||
									<label className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
									<span className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
										Security
 | 
			
		||||
									</label>
 | 
			
		||||
									</span>
 | 
			
		||||
									<div className="flex items-center mt-1">
 | 
			
		||||
										{repository.isSecure ? (
 | 
			
		||||
											<>
 | 
			
		||||
@@ -319,9 +334,9 @@ const RepositoryDetail = () => {
 | 
			
		||||
								</div>
 | 
			
		||||
								{repository.priority && (
 | 
			
		||||
									<div>
 | 
			
		||||
										<label className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
										<span className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
											Priority
 | 
			
		||||
										</label>
 | 
			
		||||
										</span>
 | 
			
		||||
										<p className="text-secondary-900 dark:text-white mt-1">
 | 
			
		||||
											{repository.priority}
 | 
			
		||||
										</p>
 | 
			
		||||
@@ -329,18 +344,18 @@ const RepositoryDetail = () => {
 | 
			
		||||
								)}
 | 
			
		||||
								{repository.description && (
 | 
			
		||||
									<div>
 | 
			
		||||
										<label className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
										<span className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
											Description
 | 
			
		||||
										</label>
 | 
			
		||||
										</span>
 | 
			
		||||
										<p className="text-secondary-900 dark:text-white mt-1">
 | 
			
		||||
											{repository.description}
 | 
			
		||||
										</p>
 | 
			
		||||
									</div>
 | 
			
		||||
								)}
 | 
			
		||||
								<div>
 | 
			
		||||
									<label className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
									<span className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
										Created
 | 
			
		||||
									</label>
 | 
			
		||||
									</span>
 | 
			
		||||
									<div className="flex items-center mt-1">
 | 
			
		||||
										<Calendar className="h-4 w-4 text-secondary-400 mr-2" />
 | 
			
		||||
										<span className="text-secondary-900 dark:text-white">
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,17 @@ const Settings = () => {
 | 
			
		||||
	const repoPrivateId = useId();
 | 
			
		||||
	const useCustomSshKeyId = useId();
 | 
			
		||||
	const isDefaultId = useId();
 | 
			
		||||
	const protocolId = useId();
 | 
			
		||||
	const hostId = useId();
 | 
			
		||||
	const portId = useId();
 | 
			
		||||
	const updateIntervalId = useId();
 | 
			
		||||
	const defaultRoleId = useId();
 | 
			
		||||
	const repositoryTypeId = useId();
 | 
			
		||||
	const githubRepoUrlId = useId();
 | 
			
		||||
	const sshKeyPathId = useId();
 | 
			
		||||
	const versionId = useId();
 | 
			
		||||
	const releaseNotesId = useId();
 | 
			
		||||
	const scriptContentId = useId();
 | 
			
		||||
	const [formData, setFormData] = useState({
 | 
			
		||||
		serverProtocol: "http",
 | 
			
		||||
		serverHost: "localhost",
 | 
			
		||||
@@ -470,10 +481,14 @@ const Settings = () => {
 | 
			
		||||
 | 
			
		||||
							<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
 | 
			
		||||
								<div>
 | 
			
		||||
									<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
 | 
			
		||||
									<label
 | 
			
		||||
										htmlFor={protocolId}
 | 
			
		||||
										className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
 | 
			
		||||
									>
 | 
			
		||||
										Protocol
 | 
			
		||||
									</label>
 | 
			
		||||
									<select
 | 
			
		||||
										id={protocolId}
 | 
			
		||||
										value={formData.serverProtocol}
 | 
			
		||||
										onChange={(e) =>
 | 
			
		||||
											handleInputChange("serverProtocol", e.target.value)
 | 
			
		||||
@@ -486,10 +501,14 @@ const Settings = () => {
 | 
			
		||||
								</div>
 | 
			
		||||
 | 
			
		||||
								<div>
 | 
			
		||||
									<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
 | 
			
		||||
									<label
 | 
			
		||||
										htmlFor={hostId}
 | 
			
		||||
										className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
 | 
			
		||||
									>
 | 
			
		||||
										Host *
 | 
			
		||||
									</label>
 | 
			
		||||
									<input
 | 
			
		||||
										id={hostId}
 | 
			
		||||
										type="text"
 | 
			
		||||
										value={formData.serverHost}
 | 
			
		||||
										onChange={(e) =>
 | 
			
		||||
@@ -510,10 +529,14 @@ const Settings = () => {
 | 
			
		||||
								</div>
 | 
			
		||||
 | 
			
		||||
								<div>
 | 
			
		||||
									<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
 | 
			
		||||
									<label
 | 
			
		||||
										htmlFor={portId}
 | 
			
		||||
										className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
 | 
			
		||||
									>
 | 
			
		||||
										Port *
 | 
			
		||||
									</label>
 | 
			
		||||
									<input
 | 
			
		||||
										id={portId}
 | 
			
		||||
										type="number"
 | 
			
		||||
										value={formData.serverPort}
 | 
			
		||||
										onChange={(e) =>
 | 
			
		||||
@@ -551,13 +574,17 @@ const Settings = () => {
 | 
			
		||||
 | 
			
		||||
							{/* Update Interval */}
 | 
			
		||||
							<div>
 | 
			
		||||
								<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
 | 
			
		||||
								<label
 | 
			
		||||
									htmlFor={updateIntervalId}
 | 
			
		||||
									className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
 | 
			
		||||
								>
 | 
			
		||||
									Agent Update Interval (minutes)
 | 
			
		||||
								</label>
 | 
			
		||||
 | 
			
		||||
								{/* Numeric input (concise width) */}
 | 
			
		||||
								<div className="flex items-center gap-2">
 | 
			
		||||
									<input
 | 
			
		||||
										id={updateIntervalId}
 | 
			
		||||
										type="number"
 | 
			
		||||
										min="5"
 | 
			
		||||
										max="1440"
 | 
			
		||||
@@ -687,10 +714,14 @@ const Settings = () => {
 | 
			
		||||
								{/* Default User Role Dropdown */}
 | 
			
		||||
								{formData.signupEnabled && (
 | 
			
		||||
									<div className="mt-3 ml-6">
 | 
			
		||||
										<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
 | 
			
		||||
										<label
 | 
			
		||||
											htmlFor={defaultRoleId}
 | 
			
		||||
											className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
 | 
			
		||||
										>
 | 
			
		||||
											Default Role for New Users
 | 
			
		||||
										</label>
 | 
			
		||||
										<select
 | 
			
		||||
											id={defaultRoleId}
 | 
			
		||||
											value={formData.defaultUserRole}
 | 
			
		||||
											onChange={(e) =>
 | 
			
		||||
												handleInputChange("defaultUserRole", e.target.value)
 | 
			
		||||
@@ -989,10 +1020,10 @@ const Settings = () => {
 | 
			
		||||
								</p>
 | 
			
		||||
 | 
			
		||||
								<div className="space-y-4">
 | 
			
		||||
									<div>
 | 
			
		||||
										<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
 | 
			
		||||
									<fieldset>
 | 
			
		||||
										<legend className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
 | 
			
		||||
											Repository Type
 | 
			
		||||
										</label>
 | 
			
		||||
										</legend>
 | 
			
		||||
										<div className="space-y-2">
 | 
			
		||||
											<div className="flex items-center">
 | 
			
		||||
												<input
 | 
			
		||||
@@ -1038,13 +1069,17 @@ const Settings = () => {
 | 
			
		||||
											Choose whether your repository is public or private to
 | 
			
		||||
											determine the appropriate access method.
 | 
			
		||||
										</p>
 | 
			
		||||
									</div>
 | 
			
		||||
									</fieldset>
 | 
			
		||||
 | 
			
		||||
									<div>
 | 
			
		||||
										<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
 | 
			
		||||
										<label
 | 
			
		||||
											htmlFor={githubRepoUrlId}
 | 
			
		||||
											className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
 | 
			
		||||
										>
 | 
			
		||||
											GitHub Repository URL
 | 
			
		||||
										</label>
 | 
			
		||||
										<input
 | 
			
		||||
											id={githubRepoUrlId}
 | 
			
		||||
											type="text"
 | 
			
		||||
											value={formData.githubRepoUrl || ""}
 | 
			
		||||
											onChange={(e) =>
 | 
			
		||||
@@ -1084,10 +1119,14 @@ const Settings = () => {
 | 
			
		||||
 | 
			
		||||
											{formData.useCustomSshKey && (
 | 
			
		||||
												<div>
 | 
			
		||||
													<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
 | 
			
		||||
													<label
 | 
			
		||||
														htmlFor={sshKeyPathId}
 | 
			
		||||
														className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
 | 
			
		||||
													>
 | 
			
		||||
														SSH Key Path
 | 
			
		||||
													</label>
 | 
			
		||||
													<input
 | 
			
		||||
														id={sshKeyPathId}
 | 
			
		||||
														type="text"
 | 
			
		||||
														value={formData.sshKeyPath || ""}
 | 
			
		||||
														onChange={(e) =>
 | 
			
		||||
@@ -1384,10 +1423,14 @@ const AgentVersionModal = ({ isOpen, onClose, onSubmit, isLoading }) => {
 | 
			
		||||
				<form onSubmit={handleSubmit} className="px-6 py-4">
 | 
			
		||||
					<div className="space-y-4">
 | 
			
		||||
						<div>
 | 
			
		||||
							<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
							<label
 | 
			
		||||
								htmlFor={versionId}
 | 
			
		||||
								className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
							>
 | 
			
		||||
								Version *
 | 
			
		||||
							</label>
 | 
			
		||||
							<input
 | 
			
		||||
								id={versionId}
 | 
			
		||||
								type="text"
 | 
			
		||||
								value={formData.version}
 | 
			
		||||
								onChange={(e) =>
 | 
			
		||||
@@ -1408,10 +1451,14 @@ const AgentVersionModal = ({ isOpen, onClose, onSubmit, isLoading }) => {
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
						<div>
 | 
			
		||||
							<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
							<label
 | 
			
		||||
								htmlFor={releaseNotesId}
 | 
			
		||||
								className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
							>
 | 
			
		||||
								Release Notes
 | 
			
		||||
							</label>
 | 
			
		||||
							<textarea
 | 
			
		||||
								id={releaseNotesId}
 | 
			
		||||
								value={formData.releaseNotes}
 | 
			
		||||
								onChange={(e) =>
 | 
			
		||||
									setFormData((prev) => ({
 | 
			
		||||
@@ -1426,7 +1473,10 @@ const AgentVersionModal = ({ isOpen, onClose, onSubmit, isLoading }) => {
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
						<div>
 | 
			
		||||
							<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
							<label
 | 
			
		||||
								htmlFor={scriptContentId}
 | 
			
		||||
								className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
							>
 | 
			
		||||
								Script Content *
 | 
			
		||||
							</label>
 | 
			
		||||
							<div className="space-y-2">
 | 
			
		||||
@@ -1437,6 +1487,7 @@ const AgentVersionModal = ({ isOpen, onClose, onSubmit, isLoading }) => {
 | 
			
		||||
									className="block w-full text-sm text-secondary-500 dark:text-secondary-400 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100 dark:file:bg-primary-900 dark:file:text-primary-200"
 | 
			
		||||
								/>
 | 
			
		||||
								<textarea
 | 
			
		||||
									id={scriptContentId}
 | 
			
		||||
									value={formData.scriptContent}
 | 
			
		||||
									onChange={(e) =>
 | 
			
		||||
										setFormData((prev) => ({
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ import {
 | 
			
		||||
	User,
 | 
			
		||||
	XCircle,
 | 
			
		||||
} from "lucide-react";
 | 
			
		||||
import React, { useState } from "react";
 | 
			
		||||
import React, { useId, useState } from "react";
 | 
			
		||||
import { useAuth } from "../contexts/AuthContext";
 | 
			
		||||
import { adminUsersAPI, permissionsAPI } from "../utils/api";
 | 
			
		||||
 | 
			
		||||
@@ -290,6 +290,13 @@ const Users = () => {
 | 
			
		||||
 | 
			
		||||
// Add User Modal Component
 | 
			
		||||
const AddUserModal = ({ isOpen, onClose, onUserCreated, roles }) => {
 | 
			
		||||
	const usernameId = useId();
 | 
			
		||||
	const emailId = useId();
 | 
			
		||||
	const firstNameId = useId();
 | 
			
		||||
	const lastNameId = useId();
 | 
			
		||||
	const passwordId = useId();
 | 
			
		||||
	const roleId = useId();
 | 
			
		||||
 | 
			
		||||
	const [formData, setFormData] = useState({
 | 
			
		||||
		username: "",
 | 
			
		||||
		email: "",
 | 
			
		||||
@@ -343,10 +350,14 @@ const AddUserModal = ({ isOpen, onClose, onUserCreated, roles }) => {
 | 
			
		||||
 | 
			
		||||
				<form onSubmit={handleSubmit} className="space-y-4">
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={usernameId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Username
 | 
			
		||||
						</label>
 | 
			
		||||
						<input
 | 
			
		||||
							id={usernameId}
 | 
			
		||||
							type="text"
 | 
			
		||||
							name="username"
 | 
			
		||||
							required
 | 
			
		||||
@@ -357,10 +368,14 @@ const AddUserModal = ({ isOpen, onClose, onUserCreated, roles }) => {
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={emailId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Email
 | 
			
		||||
						</label>
 | 
			
		||||
						<input
 | 
			
		||||
							id={emailId}
 | 
			
		||||
							type="email"
 | 
			
		||||
							name="email"
 | 
			
		||||
							required
 | 
			
		||||
@@ -372,10 +387,14 @@ const AddUserModal = ({ isOpen, onClose, onUserCreated, roles }) => {
 | 
			
		||||
 | 
			
		||||
					<div className="grid grid-cols-2 gap-4">
 | 
			
		||||
						<div>
 | 
			
		||||
							<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
							<label
 | 
			
		||||
								htmlFor={firstNameId}
 | 
			
		||||
								className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
							>
 | 
			
		||||
								First Name
 | 
			
		||||
							</label>
 | 
			
		||||
							<input
 | 
			
		||||
								id={firstNameId}
 | 
			
		||||
								type="text"
 | 
			
		||||
								name="first_name"
 | 
			
		||||
								value={formData.first_name}
 | 
			
		||||
@@ -384,10 +403,14 @@ const AddUserModal = ({ isOpen, onClose, onUserCreated, roles }) => {
 | 
			
		||||
							/>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div>
 | 
			
		||||
							<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
							<label
 | 
			
		||||
								htmlFor={lastNameId}
 | 
			
		||||
								className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
							>
 | 
			
		||||
								Last Name
 | 
			
		||||
							</label>
 | 
			
		||||
							<input
 | 
			
		||||
								id={lastNameId}
 | 
			
		||||
								type="text"
 | 
			
		||||
								name="last_name"
 | 
			
		||||
								value={formData.last_name}
 | 
			
		||||
@@ -398,10 +421,14 @@ const AddUserModal = ({ isOpen, onClose, onUserCreated, roles }) => {
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={passwordId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Password
 | 
			
		||||
						</label>
 | 
			
		||||
						<input
 | 
			
		||||
							id={passwordId}
 | 
			
		||||
							type="password"
 | 
			
		||||
							name="password"
 | 
			
		||||
							required
 | 
			
		||||
@@ -416,10 +443,14 @@ const AddUserModal = ({ isOpen, onClose, onUserCreated, roles }) => {
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={roleId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Role
 | 
			
		||||
						</label>
 | 
			
		||||
						<select
 | 
			
		||||
							id={roleId}
 | 
			
		||||
							name="role"
 | 
			
		||||
							value={formData.role}
 | 
			
		||||
							onChange={handleInputChange}
 | 
			
		||||
@@ -473,6 +504,13 @@ const AddUserModal = ({ isOpen, onClose, onUserCreated, roles }) => {
 | 
			
		||||
 | 
			
		||||
// Edit User Modal Component
 | 
			
		||||
const EditUserModal = ({ user, isOpen, onClose, onUserUpdated, roles }) => {
 | 
			
		||||
	const editUsernameId = useId();
 | 
			
		||||
	const editEmailId = useId();
 | 
			
		||||
	const editFirstNameId = useId();
 | 
			
		||||
	const editLastNameId = useId();
 | 
			
		||||
	const editRoleId = useId();
 | 
			
		||||
	const editActiveId = useId();
 | 
			
		||||
 | 
			
		||||
	const [formData, setFormData] = useState({
 | 
			
		||||
		username: user?.username || "",
 | 
			
		||||
		email: user?.email || "",
 | 
			
		||||
@@ -518,10 +556,14 @@ const EditUserModal = ({ user, isOpen, onClose, onUserUpdated, roles }) => {
 | 
			
		||||
 | 
			
		||||
				<form onSubmit={handleSubmit} className="space-y-4">
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={editUsernameId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Username
 | 
			
		||||
						</label>
 | 
			
		||||
						<input
 | 
			
		||||
							id={editUsernameId}
 | 
			
		||||
							type="text"
 | 
			
		||||
							name="username"
 | 
			
		||||
							required
 | 
			
		||||
@@ -532,10 +574,14 @@ const EditUserModal = ({ user, isOpen, onClose, onUserUpdated, roles }) => {
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={editEmailId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Email
 | 
			
		||||
						</label>
 | 
			
		||||
						<input
 | 
			
		||||
							id={editEmailId}
 | 
			
		||||
							type="email"
 | 
			
		||||
							name="email"
 | 
			
		||||
							required
 | 
			
		||||
@@ -547,10 +593,14 @@ const EditUserModal = ({ user, isOpen, onClose, onUserUpdated, roles }) => {
 | 
			
		||||
 | 
			
		||||
					<div className="grid grid-cols-2 gap-4">
 | 
			
		||||
						<div>
 | 
			
		||||
							<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
							<label
 | 
			
		||||
								htmlFor={editFirstNameId}
 | 
			
		||||
								className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
							>
 | 
			
		||||
								First Name
 | 
			
		||||
							</label>
 | 
			
		||||
							<input
 | 
			
		||||
								id={editFirstNameId}
 | 
			
		||||
								type="text"
 | 
			
		||||
								name="first_name"
 | 
			
		||||
								value={formData.first_name}
 | 
			
		||||
@@ -559,10 +609,14 @@ const EditUserModal = ({ user, isOpen, onClose, onUserUpdated, roles }) => {
 | 
			
		||||
							/>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div>
 | 
			
		||||
							<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
							<label
 | 
			
		||||
								htmlFor={editLastNameId}
 | 
			
		||||
								className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
							>
 | 
			
		||||
								Last Name
 | 
			
		||||
							</label>
 | 
			
		||||
							<input
 | 
			
		||||
								id={editLastNameId}
 | 
			
		||||
								type="text"
 | 
			
		||||
								name="last_name"
 | 
			
		||||
								value={formData.last_name}
 | 
			
		||||
@@ -573,10 +627,14 @@ const EditUserModal = ({ user, isOpen, onClose, onUserUpdated, roles }) => {
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={editRoleId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Role
 | 
			
		||||
						</label>
 | 
			
		||||
						<select
 | 
			
		||||
							id={editRoleId}
 | 
			
		||||
							name="role"
 | 
			
		||||
							value={formData.role}
 | 
			
		||||
							onChange={handleInputChange}
 | 
			
		||||
@@ -600,13 +658,17 @@ const EditUserModal = ({ user, isOpen, onClose, onUserUpdated, roles }) => {
 | 
			
		||||
 | 
			
		||||
					<div className="flex items-center">
 | 
			
		||||
						<input
 | 
			
		||||
							id={editActiveId}
 | 
			
		||||
							type="checkbox"
 | 
			
		||||
							name="is_active"
 | 
			
		||||
							checked={formData.is_active}
 | 
			
		||||
							onChange={handleInputChange}
 | 
			
		||||
							className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded"
 | 
			
		||||
						/>
 | 
			
		||||
						<label className="ml-2 block text-sm text-secondary-700 dark:text-secondary-200">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={editActiveId}
 | 
			
		||||
							className="ml-2 block text-sm text-secondary-700 dark:text-secondary-200"
 | 
			
		||||
						>
 | 
			
		||||
							Active user
 | 
			
		||||
						</label>
 | 
			
		||||
					</div>
 | 
			
		||||
@@ -649,6 +711,8 @@ const ResetPasswordModal = ({
 | 
			
		||||
	onPasswordReset,
 | 
			
		||||
	isLoading,
 | 
			
		||||
}) => {
 | 
			
		||||
	const newPasswordId = useId();
 | 
			
		||||
	const confirmPasswordId = useId();
 | 
			
		||||
	const [newPassword, setNewPassword] = useState("");
 | 
			
		||||
	const [confirmPassword, setConfirmPassword] = useState("");
 | 
			
		||||
	const [error, setError] = useState("");
 | 
			
		||||
@@ -696,10 +760,14 @@ const ResetPasswordModal = ({
 | 
			
		||||
 | 
			
		||||
				<form onSubmit={handleSubmit} className="space-y-4">
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={newPasswordId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							New Password
 | 
			
		||||
						</label>
 | 
			
		||||
						<input
 | 
			
		||||
							id={newPasswordId}
 | 
			
		||||
							type="password"
 | 
			
		||||
							required
 | 
			
		||||
							minLength={6}
 | 
			
		||||
@@ -711,10 +779,14 @@ const ResetPasswordModal = ({
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div>
 | 
			
		||||
						<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1">
 | 
			
		||||
						<label
 | 
			
		||||
							htmlFor={confirmPasswordId}
 | 
			
		||||
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
			
		||||
						>
 | 
			
		||||
							Confirm Password
 | 
			
		||||
						</label>
 | 
			
		||||
						<input
 | 
			
		||||
							id={confirmPasswordId}
 | 
			
		||||
							type="password"
 | 
			
		||||
							required
 | 
			
		||||
							value={confirmPassword}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user