}
+ */
+window.ModelConfigUtils.updateModelConfig = async function(modelId, configUpdates) {
+ return window.ErrorHandler.safeAsync(async () => {
+ const store = Alpine.store('global');
+
+ const { response, newPassword } = await window.utils.request('/api/models/config', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ modelId, config: configUpdates })
+ }, store.webuiPassword);
+
+ // Update password if server provided a new one
+ if (newPassword) {
+ store.webuiPassword = newPassword;
+ }
+
+ if (!response.ok) {
+ throw new Error('Failed to update model config');
+ }
+
+ // Optimistic update of local state
+ const dataStore = Alpine.store('data');
+ dataStore.modelConfig[modelId] = {
+ ...dataStore.modelConfig[modelId],
+ ...configUpdates
+ };
+
+ // Recompute quota rows to reflect changes
+ dataStore.computeQuotaRows();
+ }, 'Failed to update model config');
+};
diff --git a/public/js/utils/validators.js b/public/js/utils/validators.js
new file mode 100644
index 0000000..cedbb2f
--- /dev/null
+++ b/public/js/utils/validators.js
@@ -0,0 +1,168 @@
+/**
+ * Input Validation Utilities
+ * Provides validation functions for user inputs
+ */
+window.Validators = window.Validators || {};
+
+/**
+ * Validate a number is within a range
+ * @param {number} value - Value to validate
+ * @param {number} min - Minimum allowed value (inclusive)
+ * @param {number} max - Maximum allowed value (inclusive)
+ * @param {string} fieldName - Name of the field for error messages
+ * @returns {object} { isValid: boolean, value: number, error: string|null }
+ */
+window.Validators.validateRange = function(value, min, max, fieldName = 'Value') {
+ const numValue = Number(value);
+
+ if (isNaN(numValue)) {
+ return {
+ isValid: false,
+ value: min,
+ error: `${fieldName} must be a valid number`
+ };
+ }
+
+ if (numValue < min) {
+ return {
+ isValid: false,
+ value: min,
+ error: `${fieldName} must be at least ${min}`
+ };
+ }
+
+ if (numValue > max) {
+ return {
+ isValid: false,
+ value: max,
+ error: `${fieldName} must be at most ${max}`
+ };
+ }
+
+ return {
+ isValid: true,
+ value: numValue,
+ error: null
+ };
+};
+
+/**
+ * Validate a port number
+ * @param {number} port - Port number to validate
+ * @returns {object} { isValid: boolean, value: number, error: string|null }
+ */
+window.Validators.validatePort = function(port) {
+ const { PORT_MIN, PORT_MAX } = window.AppConstants.VALIDATION;
+ return window.Validators.validateRange(port, PORT_MIN, PORT_MAX, 'Port');
+};
+
+/**
+ * Validate a string is not empty
+ * @param {string} value - String to validate
+ * @param {string} fieldName - Name of the field for error messages
+ * @returns {object} { isValid: boolean, value: string, error: string|null }
+ */
+window.Validators.validateNotEmpty = function(value, fieldName = 'Field') {
+ const trimmedValue = String(value || '').trim();
+
+ if (trimmedValue.length === 0) {
+ return {
+ isValid: false,
+ value: trimmedValue,
+ error: `${fieldName} cannot be empty`
+ };
+ }
+
+ return {
+ isValid: true,
+ value: trimmedValue,
+ error: null
+ };
+};
+
+/**
+ * Validate a boolean value
+ * @param {any} value - Value to validate as boolean
+ * @returns {object} { isValid: boolean, value: boolean, error: string|null }
+ */
+window.Validators.validateBoolean = function(value) {
+ if (typeof value === 'boolean') {
+ return {
+ isValid: true,
+ value: value,
+ error: null
+ };
+ }
+
+ // Try to coerce common values
+ if (value === 'true' || value === 1 || value === '1') {
+ return { isValid: true, value: true, error: null };
+ }
+
+ if (value === 'false' || value === 0 || value === '0') {
+ return { isValid: true, value: false, error: null };
+ }
+
+ return {
+ isValid: false,
+ value: false,
+ error: 'Value must be true or false'
+ };
+};
+
+/**
+ * Validate a timeout/duration value (in milliseconds)
+ * @param {number} value - Timeout value in ms
+ * @param {number} minMs - Minimum allowed timeout (default: from constants)
+ * @param {number} maxMs - Maximum allowed timeout (default: from constants)
+ * @returns {object} { isValid: boolean, value: number, error: string|null }
+ */
+window.Validators.validateTimeout = function(value, minMs = null, maxMs = null) {
+ const { TIMEOUT_MIN, TIMEOUT_MAX } = window.AppConstants.VALIDATION;
+ return window.Validators.validateRange(value, minMs ?? TIMEOUT_MIN, maxMs ?? TIMEOUT_MAX, 'Timeout');
+};
+
+/**
+ * Validate log limit
+ * @param {number} value - Log limit value
+ * @returns {object} { isValid: boolean, value: number, error: string|null }
+ */
+window.Validators.validateLogLimit = function(value) {
+ const { LOG_LIMIT_MIN, LOG_LIMIT_MAX } = window.AppConstants.VALIDATION;
+ return window.Validators.validateRange(value, LOG_LIMIT_MIN, LOG_LIMIT_MAX, 'Log limit');
+};
+
+/**
+ * Validate and sanitize input with custom validator
+ * @param {any} value - Value to validate
+ * @param {Function} validator - Validator function
+ * @param {boolean} showError - Whether to show error toast (default: true)
+ * @returns {object} Validation result
+ */
+window.Validators.validate = function(value, validator, showError = true) {
+ const result = validator(value);
+
+ if (!result.isValid && showError && result.error) {
+ window.ErrorHandler.showError(result.error);
+ }
+
+ return result;
+};
+
+/**
+ * Create a validated input handler for Alpine.js
+ * @param {Function} validator - Validator function
+ * @param {Function} onValid - Callback when validation passes
+ * @returns {Function} Handler function
+ */
+window.Validators.createHandler = function(validator, onValid) {
+ return function(value) {
+ const result = window.Validators.validate(value, validator, true);
+
+ if (result.isValid && onValid) {
+ onValid.call(this, result.value);
+ }
+
+ return result.value;
+ };
+};
diff --git a/public/views/models.html b/public/views/models.html
index 25e3d13..1cf5b49 100644
--- a/public/views/models.html
+++ b/public/views/models.html
@@ -133,17 +133,7 @@
|
+ :data-tip="row.quotaInfo && row.quotaInfo.length > 0 ? row.quotaInfo.filter(q => q.resetTime).map(q => q.email + ': ' + q.resetTime).join('\n') : 'No reset data'">
@@ -158,7 +148,7 @@
|