- Section 2: Auto Model for Hobby/Free Plan form and handlers - Section 3: Provider Limits & Usage with configurable rate limits - Added state management for planSettings and providerLimits - Added API integration for plan-settings and provider-limits endpoints - Added populateAutoModelSelect and updateLimitModelOptions functions - Added renderProviderUsage to display usage data
602 lines
21 KiB
JavaScript
602 lines
21 KiB
JavaScript
(() => {
|
|
const pageType = document?.body?.dataset?.page || 'build';
|
|
console.log('Admin JS loaded, pageType:', pageType);
|
|
|
|
// State structure
|
|
const state = {
|
|
opencodeModels: [], // Models from OpenCode (order determines fallback)
|
|
publicModels: [], // User-facing models (completely separate)
|
|
icons: [],
|
|
availableOpencodeModels: [], // Loaded from OpenCode
|
|
planSettings: { provider: 'openrouter', freePlanModel: '', planningChain: [] },
|
|
providerLimits: {},
|
|
providerUsage: [],
|
|
};
|
|
|
|
// Element references
|
|
const el = {
|
|
// OpenCode Models
|
|
opencodeModelForm: document.getElementById('opencode-model-form'),
|
|
opencodeModelSelect: document.getElementById('opencode-model-select'),
|
|
opencodeModelLabel: document.getElementById('opencode-model-label'),
|
|
opencodeModelTier: document.getElementById('opencode-model-tier'),
|
|
opencodeModelIcon: document.getElementById('opencode-model-icon'),
|
|
opencodeModelMedia: document.getElementById('opencode-model-media'),
|
|
opencodeModelStatus: document.getElementById('opencode-model-status'),
|
|
reloadOpencodeModels: document.getElementById('reload-opencode-models'),
|
|
opencodeModelsList: document.getElementById('opencode-models-list'),
|
|
opencodeModelsCount: document.getElementById('opencode-models-count'),
|
|
|
|
// Public Models
|
|
publicModelForm: document.getElementById('public-model-form'),
|
|
publicModelName: document.getElementById('public-model-name'),
|
|
publicModelLabel: document.getElementById('public-model-label'),
|
|
publicModelTier: document.getElementById('public-model-tier'),
|
|
publicModelIcon: document.getElementById('public-model-icon'),
|
|
publicModelMedia: document.getElementById('public-model-media'),
|
|
publicModelStatus: document.getElementById('public-model-status'),
|
|
publicModelsList: document.getElementById('public-models-list'),
|
|
publicModelsCount: document.getElementById('public-models-count'),
|
|
|
|
// Auto Model Form
|
|
autoModelForm: document.getElementById('auto-model-form'),
|
|
autoModelSelect: document.getElementById('auto-model-select'),
|
|
autoModelStatus: document.getElementById('auto-model-status'),
|
|
|
|
// Provider Limits Form
|
|
providerLimitForm: document.getElementById('provider-limit-form'),
|
|
limitProvider: document.getElementById('limit-provider'),
|
|
limitScope: document.getElementById('limit-scope'),
|
|
limitModel: document.getElementById('limit-model'),
|
|
limitTpm: document.getElementById('limit-tpm'),
|
|
limitTph: document.getElementById('limit-tph'),
|
|
limitTpd: document.getElementById('limit-tpd'),
|
|
limitRpm: document.getElementById('limit-rpm'),
|
|
limitRph: document.getElementById('limit-rph'),
|
|
limitRpd: document.getElementById('limit-rpd'),
|
|
providerLimitStatus: document.getElementById('provider-limit-status'),
|
|
providerUsage: document.getElementById('provider-usage'),
|
|
|
|
// Other
|
|
iconList: document.getElementById('icon-list'),
|
|
adminRefresh: document.getElementById('admin-refresh'),
|
|
adminLogout: document.getElementById('admin-logout'),
|
|
cancelAllMessages: document.getElementById('cancel-all-messages'),
|
|
cancelMessagesStatus: document.getElementById('cancel-messages-status'),
|
|
};
|
|
|
|
// Helper functions
|
|
function setStatus(el, msg, isError = false) {
|
|
if (!el) return;
|
|
el.textContent = msg || '';
|
|
el.style.color = isError ? 'var(--danger)' : 'inherit';
|
|
}
|
|
|
|
async function api(url, options = {}) {
|
|
const res = await fetch(url, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
...options,
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Request failed');
|
|
return data;
|
|
}
|
|
|
|
// Load available models from OpenCode
|
|
async function loadAvailableOpencodeModels() {
|
|
try {
|
|
const data = await api('/api/admin/available-models');
|
|
state.availableOpencodeModels = data.models || [];
|
|
renderOpencodeModelSelect();
|
|
} catch (err) {
|
|
console.error('Failed to load OpenCode models:', err);
|
|
}
|
|
}
|
|
|
|
// Render OpenCode model dropdown
|
|
function renderOpencodeModelSelect() {
|
|
if (!el.opencodeModelSelect) return;
|
|
el.opencodeModelSelect.innerHTML = '';
|
|
|
|
if (!state.availableOpencodeModels.length) {
|
|
const opt = document.createElement('option');
|
|
opt.value = '';
|
|
opt.textContent = 'No models available';
|
|
el.opencodeModelSelect.appendChild(opt);
|
|
return;
|
|
}
|
|
|
|
state.availableOpencodeModels.forEach((m) => {
|
|
const opt = document.createElement('option');
|
|
opt.value = m.name || m.id || m;
|
|
opt.textContent = m.label || m.name || m.id || m;
|
|
el.opencodeModelSelect.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
// Render icons in selects
|
|
function renderIconOptions(selectEl) {
|
|
if (!selectEl) return;
|
|
const currentValue = selectEl.value;
|
|
selectEl.innerHTML = '<option value="">No icon</option>';
|
|
state.icons.forEach((iconPath) => {
|
|
const opt = document.createElement('option');
|
|
opt.value = iconPath;
|
|
opt.textContent = iconPath.replace('/assets/', '');
|
|
selectEl.appendChild(opt);
|
|
});
|
|
selectEl.value = currentValue;
|
|
}
|
|
|
|
// Load icons
|
|
async function loadIcons() {
|
|
try {
|
|
const data = await api('/api/admin/icons');
|
|
state.icons = data.icons || [];
|
|
renderIconOptions(el.opencodeModelIcon);
|
|
renderIconOptions(el.publicModelIcon);
|
|
renderIconLibrary();
|
|
} catch (err) {
|
|
console.error('Failed to load icons:', err);
|
|
}
|
|
}
|
|
|
|
// Render icon library
|
|
function renderIconLibrary() {
|
|
if (!el.iconList) return;
|
|
el.iconList.innerHTML = '';
|
|
|
|
if (!state.icons.length) {
|
|
el.iconList.innerHTML = '<div class="muted">No icons uploaded yet. Add files to /chat/public/assets</div>';
|
|
return;
|
|
}
|
|
|
|
state.icons.forEach((iconPath) => {
|
|
const row = document.createElement('div');
|
|
row.className = 'admin-row';
|
|
row.innerHTML = `
|
|
<div class="model-chip">
|
|
<img src="${iconPath}" alt="" style="width: 24px; height: 24px; object-fit: contain;">
|
|
<span>${iconPath.replace('/assets/', '')}</span>
|
|
</div>
|
|
`;
|
|
el.iconList.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// Load all model data
|
|
async function loadModels() {
|
|
try {
|
|
const data = await api('/api/admin/models');
|
|
state.opencodeModels = data.opencodeModels || [];
|
|
state.publicModels = data.publicModels || [];
|
|
renderOpencodeModels();
|
|
renderPublicModels();
|
|
} catch (err) {
|
|
console.error('Failed to load models:', err);
|
|
}
|
|
}
|
|
|
|
// Render OpenCode Models list
|
|
function renderOpencodeModels() {
|
|
if (!el.opencodeModelsList) return;
|
|
el.opencodeModelsList.innerHTML = '';
|
|
|
|
if (el.opencodeModelsCount) {
|
|
el.opencodeModelsCount.textContent = state.opencodeModels.length.toString();
|
|
}
|
|
|
|
if (!state.opencodeModels.length) {
|
|
el.opencodeModelsList.innerHTML = '<div class="muted">No OpenCode models added yet.</div>';
|
|
return;
|
|
}
|
|
|
|
state.opencodeModels.forEach((m, idx) => {
|
|
const row = document.createElement('div');
|
|
row.className = 'provider-row slim';
|
|
row.innerHTML = `
|
|
<div class="provider-row-header">
|
|
<div class="model-chip">
|
|
<span class="pill" style="background: ${idx === 0 ? 'var(--shopify-green)' : 'var(--primary)'}; font-weight: 700;">#${idx + 1}</span>
|
|
${m.icon ? `<img src="${m.icon}" alt="" style="width: 20px; height: 20px;">` : ''}
|
|
<span>${m.label || m.name}</span>
|
|
<span class="pill">${m.name}</span>
|
|
<span class="pill">${(m.tier || 'free').toUpperCase()} (${m.multiplier || 1}x)</span>
|
|
${m.supportsMedia ? '<span class="pill" style="background: var(--shopify-green);">Media</span>' : ''}
|
|
</div>
|
|
<div class="provider-row-actions">
|
|
<button class="ghost move-up" ${idx === 0 ? 'disabled' : ''}>↑</button>
|
|
<button class="ghost move-down" ${idx === state.opencodeModels.length - 1 ? 'disabled' : ''}>↓</button>
|
|
<button class="ghost delete-btn">Delete</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add event listeners
|
|
row.querySelector('.move-up')?.addEventListener('click', () => moveOpencodeModel(idx, -1));
|
|
row.querySelector('.move-down')?.addEventListener('click', () => moveOpencodeModel(idx, 1));
|
|
row.querySelector('.delete-btn')?.addEventListener('click', () => deleteModel(m.id, 'opencode'));
|
|
|
|
el.opencodeModelsList.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// Render Public Models
|
|
function renderPublicModels() {
|
|
if (!el.publicModelsList) return;
|
|
el.publicModelsList.innerHTML = '';
|
|
|
|
if (el.publicModelsCount) {
|
|
el.publicModelsCount.textContent = state.publicModels.length.toString();
|
|
}
|
|
|
|
if (!state.publicModels.length) {
|
|
el.publicModelsList.innerHTML = '<div class="muted">No public models added yet.</div>';
|
|
return;
|
|
}
|
|
|
|
state.publicModels.forEach((m, idx) => {
|
|
const row = document.createElement('div');
|
|
row.className = 'provider-row slim';
|
|
row.innerHTML = `
|
|
<div class="provider-row-header">
|
|
<div class="model-chip">
|
|
<span class="pill" style="background: ${idx === 0 ? 'var(--shopify-green)' : 'var(--primary)'}; font-weight: 700;">#${idx + 1}</span>
|
|
${m.icon ? `<img src="${m.icon}" alt="" style="width: 20px; height: 20px;">` : ''}
|
|
<span>${m.label || m.name}</span>
|
|
<span class="pill">${m.name}</span>
|
|
<span class="pill">${(m.tier || 'free').toUpperCase()} (${m.multiplier || 1}x)</span>
|
|
${m.supportsMedia ? '<span class="pill" style="background: var(--shopify-green);">Media</span>' : ''}
|
|
</div>
|
|
<div class="provider-row-actions">
|
|
<button class="ghost move-up" ${idx === 0 ? 'disabled' : ''}>↑</button>
|
|
<button class="ghost move-down" ${idx === state.publicModels.length - 1 ? 'disabled' : ''}>↓</button>
|
|
<button class="ghost delete-btn">Delete</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
row.querySelector('.move-up')?.addEventListener('click', () => movePublicModel(idx, -1));
|
|
row.querySelector('.move-down')?.addEventListener('click', () => movePublicModel(idx, 1));
|
|
row.querySelector('.delete-btn')?.addEventListener('click', () => deleteModel(m.id, 'public'));
|
|
|
|
el.publicModelsList.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// Move OpenCode Model
|
|
async function moveOpencodeModel(fromIdx, direction) {
|
|
const toIdx = fromIdx + direction;
|
|
if (toIdx < 0 || toIdx >= state.opencodeModels.length) return;
|
|
|
|
const newOrder = [...state.opencodeModels];
|
|
const [item] = newOrder.splice(fromIdx, 1);
|
|
newOrder.splice(toIdx, 0, item);
|
|
|
|
try {
|
|
await api('/api/admin/models/reorder', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ type: 'opencode', models: newOrder }),
|
|
});
|
|
state.opencodeModels = newOrder;
|
|
renderOpencodeModels();
|
|
} catch (err) {
|
|
console.error('Failed to reorder:', err);
|
|
}
|
|
}
|
|
|
|
// Move Public Model
|
|
async function movePublicModel(fromIdx, direction) {
|
|
const toIdx = fromIdx + direction;
|
|
if (toIdx < 0 || toIdx >= state.publicModels.length) return;
|
|
|
|
const newOrder = [...state.publicModels];
|
|
const [item] = newOrder.splice(fromIdx, 1);
|
|
newOrder.splice(toIdx, 0, item);
|
|
|
|
try {
|
|
await api('/api/admin/models/reorder', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ type: 'public', models: newOrder }),
|
|
});
|
|
state.publicModels = newOrder;
|
|
renderPublicModels();
|
|
} catch (err) {
|
|
console.error('Failed to reorder:', err);
|
|
}
|
|
}
|
|
|
|
// Delete Model
|
|
async function deleteModel(id, type) {
|
|
try {
|
|
await api(`/api/admin/models/${id}?type=${type}`, { method: 'DELETE' });
|
|
await loadModels();
|
|
} catch (err) {
|
|
console.error('Failed to delete:', err);
|
|
}
|
|
}
|
|
|
|
// Form Handlers
|
|
if (el.opencodeModelForm) {
|
|
el.opencodeModelForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const model = el.opencodeModelSelect.value;
|
|
const label = el.opencodeModelLabel.value.trim();
|
|
|
|
if (!model || !label) {
|
|
setStatus(el.opencodeModelStatus, 'Model and display name are required', true);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await api('/api/admin/models', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
type: 'opencode',
|
|
name: model,
|
|
label,
|
|
tier: el.opencodeModelTier?.value || 'free',
|
|
icon: el.opencodeModelIcon?.value || '',
|
|
supportsMedia: el.opencodeModelMedia?.checked || false,
|
|
}),
|
|
});
|
|
setStatus(el.opencodeModelStatus, 'Added');
|
|
el.opencodeModelLabel.value = '';
|
|
await loadModels();
|
|
} catch (err) {
|
|
setStatus(el.opencodeModelStatus, err.message, true);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (el.reloadOpencodeModels) {
|
|
el.reloadOpencodeModels.addEventListener('click', async () => {
|
|
setStatus(el.opencodeModelStatus, 'Loading...');
|
|
await loadAvailableOpencodeModels();
|
|
setStatus(el.opencodeModelStatus, 'Loaded');
|
|
setTimeout(() => setStatus(el.opencodeModelStatus, ''), 1500);
|
|
});
|
|
}
|
|
|
|
if (el.publicModelForm) {
|
|
el.publicModelForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const name = el.publicModelName.value.trim();
|
|
const label = el.publicModelLabel.value.trim();
|
|
|
|
if (!name || !label) {
|
|
setStatus(el.publicModelStatus, 'Model ID and display name are required', true);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await api('/api/admin/models', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
type: 'public',
|
|
name,
|
|
label,
|
|
tier: el.publicModelTier?.value || 'free',
|
|
icon: el.publicModelIcon?.value || '',
|
|
supportsMedia: el.publicModelMedia?.checked || false,
|
|
}),
|
|
});
|
|
setStatus(el.publicModelStatus, 'Added');
|
|
el.publicModelName.value = '';
|
|
el.publicModelLabel.value = '';
|
|
await loadModels();
|
|
} catch (err) {
|
|
setStatus(el.publicModelStatus, err.message, true);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize
|
|
// Load plan settings
|
|
async function loadPlanSettings() {
|
|
try {
|
|
const data = await api('/api/admin/plan-settings');
|
|
state.planSettings = data || { provider: 'openrouter', freePlanModel: '', planningChain: [] };
|
|
populateAutoModelSelect();
|
|
if (el.autoModelSelect && state.planSettings.freePlanModel) {
|
|
el.autoModelSelect.value = state.planSettings.freePlanModel;
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load plan settings:', err);
|
|
}
|
|
}
|
|
|
|
// Populate auto model select dropdown
|
|
function populateAutoModelSelect() {
|
|
if (!el.autoModelSelect) return;
|
|
const currentValue = el.autoModelSelect.value;
|
|
el.autoModelSelect.innerHTML = '<option value="">Auto (use first configured model)</option>';
|
|
|
|
state.opencodeModels.forEach((m) => {
|
|
const opt = document.createElement('option');
|
|
opt.value = m.name;
|
|
opt.textContent = `${m.label || m.name} (${m.name})`;
|
|
el.autoModelSelect.appendChild(opt);
|
|
});
|
|
|
|
el.autoModelSelect.value = currentValue;
|
|
}
|
|
|
|
// Load provider limits
|
|
async function loadProviderLimits() {
|
|
try {
|
|
const data = await api('/api/admin/provider-limits');
|
|
state.providerLimits = data.limits || {};
|
|
state.providerUsage = data.usage || [];
|
|
renderProviderUsage();
|
|
} catch (err) {
|
|
console.error('Failed to load provider limits:', err);
|
|
}
|
|
}
|
|
|
|
// Render provider usage
|
|
function renderProviderUsage() {
|
|
if (!el.providerUsage) return;
|
|
el.providerUsage.innerHTML = '';
|
|
|
|
if (!state.providerUsage.length) {
|
|
el.providerUsage.innerHTML = '<div class="muted">No usage data available.</div>';
|
|
return;
|
|
}
|
|
|
|
state.providerUsage.forEach((usage) => {
|
|
const row = document.createElement('div');
|
|
row.className = 'admin-row';
|
|
row.innerHTML = `
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
<span class="pill">${usage.provider}</span>
|
|
<span>${usage.tokens || 0} tokens / ${usage.requests || 0} requests</span>
|
|
</div>
|
|
`;
|
|
el.providerUsage.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// Update limit model options based on provider selection
|
|
function updateLimitModelOptions() {
|
|
if (!el.limitModel || !el.limitProvider) return;
|
|
const provider = el.limitProvider.value;
|
|
const currentValue = el.limitModel.value;
|
|
|
|
el.limitModel.innerHTML = '<option value="">Any model</option>';
|
|
|
|
// Add models from opencodeModels that match this provider
|
|
state.opencodeModels.forEach((m) => {
|
|
if (m.name && m.name.includes('/')) {
|
|
const modelProvider = m.name.split('/')[0];
|
|
if (modelProvider === provider) {
|
|
const opt = document.createElement('option');
|
|
opt.value = m.name;
|
|
opt.textContent = m.label || m.name;
|
|
el.limitModel.appendChild(opt);
|
|
}
|
|
}
|
|
});
|
|
|
|
el.limitModel.value = currentValue;
|
|
}
|
|
|
|
async function init() {
|
|
await loadIcons();
|
|
await loadAvailableOpencodeModels();
|
|
await loadModels();
|
|
await loadPlanSettings();
|
|
await loadProviderLimits();
|
|
}
|
|
|
|
if (el.adminRefresh) {
|
|
el.adminRefresh.addEventListener('click', init);
|
|
}
|
|
|
|
if (el.adminLogout) {
|
|
el.adminLogout.addEventListener('click', async () => {
|
|
await api('/api/admin/logout', { method: 'POST' });
|
|
window.location.href = '/admin/login';
|
|
});
|
|
}
|
|
|
|
if (el.cancelAllMessages) {
|
|
el.cancelAllMessages.addEventListener('click', async () => {
|
|
if (!confirm('Are you sure you want to cancel all running and queued messages?')) return;
|
|
try {
|
|
await api('/api/admin/cancel-all-messages', { method: 'POST' });
|
|
setStatus(el.cancelMessagesStatus, 'All messages cancelled');
|
|
} catch (err) {
|
|
setStatus(el.cancelMessagesStatus, err.message, true);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Auto Model Form Handler
|
|
if (el.autoModelForm) {
|
|
el.autoModelForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const selectedModel = el.autoModelSelect.value;
|
|
|
|
try {
|
|
await api('/api/admin/plan-settings', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
...state.planSettings,
|
|
freePlanModel: selectedModel,
|
|
}),
|
|
});
|
|
setStatus(el.autoModelStatus, 'Saved');
|
|
setTimeout(() => setStatus(el.autoModelStatus, ''), 1500);
|
|
} catch (err) {
|
|
setStatus(el.autoModelStatus, err.message, true);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Provider Limit Form Handler
|
|
if (el.providerLimitForm) {
|
|
// Update model options when provider changes
|
|
el.limitProvider?.addEventListener('change', () => {
|
|
updateLimitModelOptions();
|
|
// Load existing limits for this provider if any
|
|
const provider = el.limitProvider.value;
|
|
const scope = el.limitScope.value;
|
|
const model = el.limitModel.value;
|
|
const limits = state.providerLimits[provider];
|
|
if (limits) {
|
|
const target = scope === 'model' && model ? (limits.perModel?.[model] || {}) : limits;
|
|
if (el.limitTpm) el.limitTpm.value = target.tokensPerMinute || '';
|
|
if (el.limitTph) el.limitTph.value = target.tokensPerHour || '';
|
|
if (el.limitTpd) el.limitTpd.value = target.tokensPerDay || '';
|
|
if (el.limitRpm) el.limitRpm.value = target.requestsPerMinute || '';
|
|
if (el.limitRph) el.limitRph.value = target.requestsPerHour || '';
|
|
if (el.limitRpd) el.limitRpd.value = target.requestsPerDay || '';
|
|
}
|
|
});
|
|
|
|
// Update form when scope changes
|
|
el.limitScope?.addEventListener('change', () => {
|
|
if (el.limitModel) {
|
|
el.limitModel.disabled = el.limitScope.value !== 'model';
|
|
if (el.limitScope.value !== 'model') el.limitModel.value = '';
|
|
}
|
|
});
|
|
|
|
el.providerLimitForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const provider = el.limitProvider?.value;
|
|
const scope = el.limitScope?.value;
|
|
const model = el.limitModel?.value;
|
|
|
|
const limits = {
|
|
tokensPerMinute: parseInt(el.limitTpm?.value) || 0,
|
|
tokensPerHour: parseInt(el.limitTph?.value) || 0,
|
|
tokensPerDay: parseInt(el.limitTpd?.value) || 0,
|
|
requestsPerMinute: parseInt(el.limitRpm?.value) || 0,
|
|
requestsPerHour: parseInt(el.limitRph?.value) || 0,
|
|
requestsPerDay: parseInt(el.limitRpd?.value) || 0,
|
|
};
|
|
|
|
try {
|
|
const payload = {
|
|
provider,
|
|
scope,
|
|
model: scope === 'model' ? model : null,
|
|
limits,
|
|
};
|
|
await api('/api/admin/provider-limits', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
setStatus(el.providerLimitStatus, 'Saved');
|
|
await loadProviderLimits();
|
|
setTimeout(() => setStatus(el.providerLimitStatus, ''), 1500);
|
|
} catch (err) {
|
|
setStatus(el.providerLimitStatus, err.message, true);
|
|
}
|
|
});
|
|
}
|
|
|
|
init();
|
|
})(); |