<!--
Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
-->
{% extends "base.html" %}

{% block title %}Rotations - AISBF Dashboard{% endblock %}

{% block content %}
<h2 style="margin-bottom: 30px;">Rotations Configuration</h2>

{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}

{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}

<div class="form-group" style="margin-bottom: 20px;">
    <label>
        <input type="checkbox" id="global-notify" onchange="updateGlobalNotify(this.checked)">
        Global Notify Errors — return errors as messages (not HTTP 503) when all providers fail
    </label>
</div>

<div id="rotations-list" style="margin-bottom: 20px;">
    <!-- Rotations list will be rendered here -->
</div>

<button type="button" class="btn" onclick="addRotation()" style="margin-top: 20px;"data-i18n="user_rotations_page.add_rotation">Add Rotation</button>
<div style="display: flex; gap: 10px; margin-top: 20px;">
    <button type="button" class="btn" onclick="saveRotations()"data-i18n="user_rotations_page.save_config">Save Configuration</button>
    <a href="{{ url_for(request, '/dashboard') }}" class="btn btn-secondary">Cancel</a>
</div>

<script src="{{ url_for(request, '/dashboard/static/dragsort.js') }}"></script>
<script>
function escHtmlAttr(s) {
    return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}

const rotationsData = {
    config: {{ rotations_json | safe }},
    providers: {{ available_providers | safe }},
    saveUrl: "{{ url_for(request, '/dashboard/rotations') }}",
    successUrl: "{{ url_for(request, '/dashboard/rotations?success=1') }}"
};
let rotationsConfig = rotationsData.config;
let availableProviders = rotationsData.providers;
// Meta-providers: a rotation entry can forward to a named rotation/autoselect
// instead of a real provider. The model "name" then holds the target id.
const META_PROVIDERS = ['rotations', 'autoselect'];
const providersMeta = {{ providers_meta | default('{}') | safe }};
function _loadExpanded(storeKey) {
    try { return new Set(JSON.parse(sessionStorage.getItem(storeKey) || '[]')); }
    catch (e) { return new Set(); }
}
let expandedRotations = _loadExpanded('aisbf_expanded_rotations');
function _saveExpandedRotations() {
    try { sessionStorage.setItem('aisbf_expanded_rotations', JSON.stringify([...expandedRotations])); }
    catch (e) { /* ignore */ }
}
let rotationMasterOrder = Object.keys(rotationsConfig.rotations || {});
let _rotationDS = null;

// ===== Provider Usage =====
const _usageCache = {};
const BASE_PATH = {{ (request.scope.get('root_path', '') or '') | tojson }};

function _providerSupportsUsage(pid) { return providersMeta[pid] && providersMeta[pid].type === 'codex'; }
function _fmtSeconds(s) {
    if (!s || s <= 0) return '0s';
    const h = Math.floor(s/3600), m = Math.floor((s%3600)/60), sec = s%60;
    if (h > 0) return `${h}h${m > 0 ? ' '+m+'m' : ''}`;
    if (m > 0) return `${m}m${sec > 0 ? ' '+sec+'s' : ''}`;
    return `${sec}s`;
}
function _windowLabel(seconds) {
    if (!seconds || seconds <= 0) return 'Window';
    if (seconds < 120) return 'Session';
    const MIN = 60, HOUR = 3600, DAY = 86400, WEEK = 604800, MONTH = 2592000;
    if (seconds % MONTH === 0) return seconds === MONTH ? 'Monthly' : `${seconds/MONTH}mo`;
    if (seconds % WEEK  === 0) return seconds === WEEK  ? 'Weekly'  : `${seconds/WEEK}wk`;
    if (seconds % DAY   === 0) return seconds === DAY   ? 'Daily'   : `${seconds/DAY}d`;
    if (seconds % HOUR  === 0) return seconds === HOUR  ? 'Hourly'  : `${seconds/HOUR}h`;
    if (seconds % MIN   === 0) return `${seconds/MIN}min`;
    if (seconds >= MONTH * 0.9) return `${Math.round(seconds/MONTH)}mo`;
    if (seconds >= WEEK  * 0.9) return 'Weekly';
    if (seconds >= DAY)         return `${Math.round(seconds/DAY)}d`;
    if (seconds >= HOUR)        return `${Math.round(seconds/HOUR)}h`;
    return `${Math.round(seconds/MIN)}min`;
}
function _usageBarHtml(pct) {
    const c = pct >= 90 ? '#ef4444' : pct >= 70 ? '#f59e0b' : '#22c55e';
    return `<div style="height:5px;background:var(--color-border);border-radius:2px;overflow:hidden;min-width:50px;flex:1;"><div style="height:100%;width:${Math.min(pct,100)}%;background:${c};"></div></div>`;
}
function _renderUsageCompact(usage) {
    if (!usage) return '';
    const parts = [];
    const plan = usage.plan_type ? `<span style="font-size:10px;color:var(--color-muted);text-transform:uppercase;">${usage.plan_type}</span>` : '';
    const rl = usage.rate_limit;
    if (rl) {
        const windows = [];
        if (rl.primary_window) windows.push(rl.primary_window);
        if (rl.secondary_window) windows.push(rl.secondary_window);
        if (Array.isArray(rl.additional_rate_limits)) windows.push(...rl.additional_rate_limits);
        windows.forEach(w => {
            const pct = w.used_percent || 0;
            let countStr = '';
            if (w.num_requests_used != null && w.num_requests_limit != null) {
                countStr = ` (${w.num_requests_used}/${w.num_requests_limit})`;
            }
            parts.push(`<div style="display:flex;align-items:center;gap:3px;font-size:10px;"><span style="color:var(--color-muted);white-space:nowrap;">${_windowLabel(w.limit_window_seconds)}:</span>${_usageBarHtml(pct)}<span>${pct}%${countStr}</span></div>`);
        });
        if (rl.limit_reached) parts.push('<span style="color:#ef4444;font-size:10px;font-weight:700;">LIMIT</span>');
    }
    return `<div style="display:inline-flex;flex-direction:column;gap:2px;vertical-align:middle;margin-left:4px;">${plan}${parts.join('')}</div>`;
}
async function fetchProviderUsage(pid) {
    try {
        const r = await fetch(`${BASE_PATH}/dashboard/api/provider/${encodeURIComponent(pid)}/usage`);
        if (!r.ok) return null;
        const d = await r.json();
        if (d.supported === false) return null;
        _usageCache[pid] = {data: d.usage, ts: Date.now()};
        return d.usage;
    } catch(e) { return null; }
}
function _updateRotationUsageBadges() {
    Object.keys(_usageCache).forEach(pid => {
        document.querySelectorAll(`[data-usage-pid="${pid}"]`).forEach(el => {
            const c = _usageCache[pid];
            if (c && c.data) el.innerHTML = _renderUsageCompact(c.data);
        });
    });
}
async function _refreshRotationsUsage() {
    const pids = new Set();
    Object.values(rotationsConfig.rotations || {}).forEach(rot => {
        (rot.providers || []).forEach(p => { if (p.provider_id && _providerSupportsUsage(p.provider_id)) pids.add(p.provider_id); });
    });
    for (const pid of pids) {
        const c = _usageCache[pid];
        if (c && (Date.now() - c.ts) < 14000) continue;
        await fetchProviderUsage(pid);
    }
    _updateRotationUsageBadges();
}
setInterval(_refreshRotationsUsage, 15000);
document.addEventListener('DOMContentLoaded', () => setTimeout(_refreshRotationsUsage, 500));

// ===== Model Search Modal =====
let _msAllItems = [], _msDispItems = [], _msCancelled = false, _msCallback = null;

function _msEnsure() {
    if (document.getElementById('ms-overlay')) return;
    const d = document.createElement('div');
    d.id = 'ms-overlay';
    d.style.cssText = 'display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.75);z-index:9999;align-items:center;justify-content:center;';
    d.innerHTML = `
      <div style="background:var(--bg-page);border:1px solid var(--color-border);border-radius:8px;padding:24px;max-width:560px;width:93%;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,0.6);">
        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
          <h3 id="ms-title" style="margin:0;font-size:16px;color:var(--color-text);"></h3>
          <button type="button" onclick="msClose()" style="background:none;border:none;color:var(--color-muted);cursor:pointer;font-size:22px;padding:0 4px;">&times;</button>
        </div>
        <div id="ms-status" style="font-size:13px;color:var(--color-muted);min-height:18px;margin-bottom:10px;"></div>
        <div style="display:flex;gap:8px;margin-bottom:12px;">
          <input type="text" id="ms-filter-input" placeholder="${window.i18n.t('rotations.filter_results')}"
            style="flex:1;padding:8px;border:1px solid var(--color-border);border-radius:3px;background:var(--bg-panel);color:var(--color-text);font-size:14px;"
            onkeydown="if(event.key==='Enter')msFilter()">
          <button type="button" onclick="msFilter()" class="btn btn-secondary" style="padding:8px 14px;font-size:13px;">${window.i18n.t('rotations.filter')}</button>
        </div>
        <div id="ms-results" style="overflow-y:auto;flex:1;max-height:340px;border:1px solid var(--color-border);border-radius:3px;background:var(--bg-panel);min-height:40px;"></div>
        <div style="margin-top:14px;display:flex;justify-content:flex-end;">
          <button type="button" onclick="msClose()" class="btn btn-secondary">${window.i18n.t('rotations.cancel')}</button>
        </div>
      </div>`;
    document.body.appendChild(d);
}

async function msOpenProvider(providerId, inputId, cb) {
    _msEnsure(); _msCancelled = false; _msAllItems = []; _msDispItems = [];
    _msCallback = function(v) { const e = document.getElementById(inputId); if (e) e.value = v; cb(v); };
    const o = document.getElementById('ms-overlay'); o.style.display = 'flex';
    document.getElementById('ms-title').textContent = window.i18n.interpolate(window.i18n.t('rotations.search_models_title'), {provider: providerId});
    document.getElementById('ms-filter-input').value = '';
    document.getElementById('ms-status').textContent = window.i18n.t('rotations.checking_models');
    document.getElementById('ms-results').innerHTML = '<p style="color:var(--color-muted);text-align:center;padding:20px;">' + window.i18n.t('rotations.loading') + '</p>';
    try {
        let r = await fetch(`${BASE_PATH}/dashboard/providers/${encodeURIComponent(providerId)}/configured-models`);
        if (_msCancelled) return;
        let d = await r.json(); let m = d.models || [];
        if (!m.length) {
            document.getElementById('ms-status').textContent = window.i18n.t('rotations.fetching_from_api');
            r = await fetch(`${BASE_PATH}/dashboard/providers/${encodeURIComponent(providerId)}/search-models?refresh=true`);
            if (_msCancelled) return;
            d = await r.json(); m = d.models || [];
        }
        if (_msCancelled) return;
        _msAllItems = m;
        document.getElementById('ms-status').textContent = m.length ? m.length + ' model(s) found. ' + window.i18n.t('rotations.models_found_filter') : window.i18n.t('rotations.no_models_found');
        msRenderResults(m);
    } catch(e) {
        if (!_msCancelled) { document.getElementById('ms-status').textContent = 'Error: ' + e.message; document.getElementById('ms-results').innerHTML = '<p style="color:#f87171;padding:15px;">' + window.i18n.t('rotations.failed_load') + '</p>'; }
    }
}

function msFilter() {
    const q = document.getElementById('ms-filter-input').value.trim().toLowerCase();
    document.getElementById('ms-status').textContent = window.i18n.t('rotations.searching');
    const filtered = q ? _msAllItems.filter(m => (typeof m === 'string' ? m : m.id + ' ' + m.name).toLowerCase().includes(q)) : _msAllItems;
    document.getElementById('ms-status').textContent = window.i18n.interpolate(window.i18n.t('rotations.result_count'), {n: filtered.length});
    msRenderResults(filtered);
}

function msRenderResults(items) {
    _msDispItems = items;
    const el = document.getElementById('ms-results');
    if (!items || !items.length) { el.innerHTML = '<p style="color:var(--color-muted);text-align:center;padding:20px;">' + window.i18n.t('rotations.no_results') + '</p>'; return; }
    el.innerHTML = items.map((item, i) => {
        const isStr = typeof item === 'string';
        const id = isStr ? item : item.id, label = isStr ? item : item.name;
        return `<div data-idx="${i}" onclick="msPickIdx(this)" style="padding:8px 12px;cursor:pointer;border-bottom:1px solid var(--color-border);font-size:13px;word-break:break-all;" onmouseover="this.style.background='var(--bg-accent)'" onmouseout="this.style.background=''">` +
               (id !== label ? `<span style="color:var(--color-text);">${escHtmlAttr(id)}</span><span style="color:var(--color-subtle);font-size:11px;margin-left:6px;">${escHtmlAttr(label)}</span>` : `<span>${escHtmlAttr(id)}</span>`) + '</div>';
    }).join('');
}

function msPickIdx(el) {
    const i = parseInt(el.getAttribute('data-idx'));
    const item = _msDispItems[i]; const val = typeof item === 'string' ? item : item.id;
    if (_msCallback) _msCallback(val);
    msClose();
}

function msClose() {
    _msCancelled = true;
    const el = document.getElementById('ms-overlay'); if (el) el.style.display = 'none';
}

function msOpenProviderForInput(btn) {
    const providerId = btn.getAttribute('data-provider');
    const inputId = btn.getAttribute('data-input-id');
    const input = document.getElementById(inputId);
    msOpenProvider(providerId, inputId, function(v) {
        if (input) { input.value = v; input.dispatchEvent(new Event('change')); }
    });
}

// Provider select widget: <select> for ≤25, datalist input for >25
function buildProviderSelectHtml(uid, currentValue, onChangeFn) {
    const metaOpts = META_PROVIDERS.map(p =>
        `<option value="${escHtmlAttr(p)}" ${currentValue === p ? 'selected' : ''}>${escHtmlAttr(p)}</option>`
    ).join('');
    if (availableProviders.length <= 25) {
        const opts = availableProviders.map(p =>
            `<option value="${escHtmlAttr(p)}" ${currentValue === p ? 'selected' : ''}>${escHtmlAttr(p)}</option>`
        ).join('');
        return `<select id="${uid}" onchange="(${onChangeFn})(this.value)" required>
            <option value="">${window.i18n.t('rotations.select_provider')}</option>
            <optgroup label="${window.i18n.t('rotations.forward_to') || 'Forward to'}">${metaOpts}</optgroup>${opts}</select>`;
    } else {
        const dlOpts = (META_PROVIDERS.concat(availableProviders)).map(p => `<option value="${escHtmlAttr(p)}">`).join('');
        return `<div style="position:relative;">
            <input type="text" id="${uid}" value="${escHtmlAttr(currentValue)}" list="${uid}-dl"
                   placeholder="${window.i18n.t('rotations.type_search_provider')}"
                   oninput="handleProviderInput('${uid}', this.value, (${onChangeFn}))"
                   onchange="handleProviderInput('${uid}', this.value, (${onChangeFn}))"
                   style="width:100%;padding:8px;border:1px solid var(--color-border);border-radius:3px;background:var(--bg-page);color:var(--color-text);font-size:14px;">
            <datalist id="${uid}-dl">${dlOpts}</datalist>
        </div>`;
    }
}

function handleProviderInput(uid, value, callback) {
    if (availableProviders.includes(value) || META_PROVIDERS.includes(value) || value === '') {
        callback(value);
    }
}

document.getElementById('global-notify').checked = rotationsConfig.notifyerrors || false;

function renderRotationsList() {
    const container = document.getElementById('rotations-list');
    container.innerHTML = '';

    rotationMasterOrder.filter(k => k in (rotationsConfig.rotations || {})).forEach(key => {
        const rotation = rotationsConfig.rotations[key];
        const rotationItem = document.createElement('div');
        rotationItem.className = 'rotation-item';
        rotationItem.dataset.sortKey = key;
        rotationItem.style.cssText = 'border: 1px solid var(--color-border); margin-bottom: 10px; border-radius: 5px; background: var(--bg-page);';

        const isExpanded = expandedRotations.has(key);
        const providerCount = rotation.providers ? rotation.providers.length : 0;
        const safeKey = key.replace(/\\/g, '\\\\').replace(/'/g, "\\'");

        rotationItem.innerHTML = `
            <div class="rotation-header" onclick="toggleRotation('${safeKey}')" style="padding: 15px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; user-select: none;">
                <div style="display: flex; align-items: center; gap: 10px;">
                    <span class="drag-handle" onclick="event.stopPropagation();" title="Drag to reorder">⠿</span>
                    <span style="font-size: 18px;">${isExpanded ? '▼' : '▶'}</span>
                    <strong style="font-size: 16px;">${escHtmlAttr(key)}</strong>
                    <span style="color: var(--color-muted); font-size: 14px;">(${providerCount} ${window.i18n.t(providerCount !== 1 ? 'rotations.providers_plural' : 'rotations.providers_singular')})</span>
                </div>
                <div style="display: flex; gap: 8px;">
                    <button type="button" class="btn btn-secondary" onclick="event.stopPropagation(); copyRotation('${safeKey}')" style="padding: 5px 15px;">${window.i18n.t('rotations.copy')}</button>
                    <button type="button" class="btn btn-secondary" onclick="event.stopPropagation(); removeRotation('${safeKey}')" style="background: #dc3545; padding: 5px 15px;">${window.i18n.t('rotations.remove')}</button>
                </div>
            </div>
            <div id="rotation-details-${escHtmlAttr(key)}" style="display: ${isExpanded ? 'block' : 'none'}; padding: 20px; border-top: 1px solid var(--color-border); background: var(--bg-panel);">
            </div>
        `;

        container.appendChild(rotationItem);

        if (isExpanded) {
            renderRotationDetails(key);
        }
    });

    if (!_rotationDS) {
        _rotationDS = new DragSort({
            containerId: 'rotations-list',
            masterOrder: { value: rotationMasterOrder },
            onReorder: function(newOrder) {
                rotationMasterOrder = newOrder;
                renderRotationsList();
                fetch(BASE_PATH + '/dashboard/api/rotation/reorder', {
                    method: 'POST', headers: {'Content-Type':'application/json'},
                    body: JSON.stringify({order: newOrder})
                }).catch(function(){});
            }
        });
    } else {
        _rotationDS._opts.masterOrder.value = rotationMasterOrder;
        _rotationDS.attach();
    }
}

document.addEventListener('i18n:ready', renderRotationsList);

function toggleRotation(key) {
    if (expandedRotations.has(key)) {
        expandedRotations.delete(key);
    } else {
        expandedRotations.add(key);
    }
    _saveExpandedRotations();
    renderRotationsList();
}

function renderRotationDetails(rotationKey) {
    const container = document.getElementById(`rotation-details-${rotationKey}`);
    const rotation = rotationsConfig.rotations[rotationKey];
    const safeRKey = rotationKey.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
    const inheritedCaps = Array.isArray(rotation.capabilities) ? rotation.capabilities : [];
    const partialCaps = Array.isArray(rotation.partial_capabilities) ? rotation.partial_capabilities : [];
    const inheritedCapsHtml = inheritedCaps.length
        ? inheritedCaps.map(cap => `<span style="display:inline-block;margin:2px 6px 0 0;padding:3px 8px;border-radius:999px;background:var(--bg-accent);font-size:12px;">${escHtmlAttr(cap)}</span>`).join('')
        : '<span style="color: var(--color-muted);">No capability is currently shared by every model in this rotation.</span>';
    const partialCapsHtml = partialCaps.length
        ? `<div style="margin-top:8px;font-size:12px;color:var(--color-muted);">Partial across only some models: ${partialCaps.map(cap => escHtmlAttr(cap)).join(', ')}</div>`
        : '';
    
    const advancedInner = `
        <div class="form-group">
            <label>${window.i18n.t('rotations.default_rate_limit')}</label>
            <input type="number" value="${rotation.default_rate_limit || ''}" onchange="updateRotation('${rotationKey}', 'default_rate_limit', this.value ? parseFloat(this.value) : null)" step="0.1" placeholder="${window.i18n.t('rotations.optional')}">
        </div>

        <div class="form-group">
            <label>${window.i18n.t('rotations.default_context_size')}</label>
            <input type="number" value="${rotation.default_context_size || ''}" onchange="updateRotation('${rotationKey}', 'default_context_size', this.value ? parseInt(this.value) : null)" placeholder="${window.i18n.t('rotations.optional')}">
        </div>

        <h4 style="margin-top: 14px; margin-bottom: 10px;">Feature Overrides</h4>
        ${triStateCompact('Context Condensation', rotation.enable_context_condensation, `updateRotationTriState('${rotationKey}', 'enable_context_condensation', this.value)`)}
        ${triStateCompact('Response Cache', rotation.enable_response_cache, `updateRotationTriState('${rotationKey}', 'enable_response_cache', this.value)`)}
        ${triStateCompact('Prompt Batching', rotation.enable_prompt_batching, `updateRotationTriState('${rotationKey}', 'enable_prompt_batching', this.value)`)}
        ${triStateCompact('Prompt Security', rotation.enable_prompt_security, `updateRotationTriState('${rotationKey}', 'enable_prompt_security', this.value)`)}
        ${triStateCompact('Context Lens Analytics', rotation.enable_context_lens, `updateRotationTriState('${rotationKey}', 'enable_context_lens', this.value)`)}
        ${triStateCompact('Block High-Risk Prompts', rotation.block_high_risk_prompts, `updateRotationTriState('${rotationKey}', 'block_high_risk_prompts', this.value)`)}
        ${triStateCompact('NSFW Classification', rotation.enable_nsfw_classification, `updateRotationTriState('${rotationKey}', 'enable_nsfw_classification', this.value)`)}
        ${triStateCompact('Privacy Classification', rotation.enable_privacy_classification, `updateRotationTriState('${rotationKey}', 'enable_privacy_classification', this.value)`)}
    `;

    container.innerHTML = `
        <div class="form-group">
            <label>${window.i18n.t('rotations.model_name')}</label>
            <input type="text" value="${rotation.model_name}" onchange="updateRotation('${rotationKey}', 'model_name', this.value)" required>
        </div>

        <div class="form-group">
            <label>
                <input type="checkbox" ${rotation.notifyerrors ? 'checked' : ''} onchange="updateRotation('${rotationKey}', 'notifyerrors', this.checked)">
                ${window.i18n.t('rotations.notify_errors')} — ${window.i18n.t('rotations.notify_errors_desc')}
            </label>
        </div>

        <div class="form-group">
            <label>${window.i18n.t('rotations.capabilities')}</label>
            <input type="text" value="${rotation.capabilities ? rotation.capabilities.join(', ') : ''}" onchange="updateRotationCapabilities('${rotationKey}', this.value)" placeholder="${window.i18n.t('rotations.capabilities_placeholder')}">
            <small style="color: var(--color-muted); display:block; margin-top:5px;">This field is auto-inherited from the capabilities shared by all models in the rotation when you save.</small>
            <div style="margin-top:8px;">${inheritedCapsHtml}</div>
            ${partialCapsHtml}
        </div>

        ${subPanel(`rot:${rotationKey}:advanced`, 'Advanced configuration', advancedInner)}

        <h4 style="margin-top: 20px; margin-bottom: 10px;">${window.i18n.t('rotations.providers_section')}</h4>
        <div id="providers-${rotationKey}"></div>
        <button type="button" class="btn btn-secondary" onclick="addRotationProvider('${safeRKey}')" style="margin-top: 10px;">${window.i18n.t('rotations.add_provider')}</button>

        <div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--color-border); display: flex; gap: 10px; align-items: center;">
            <button type="button" class="btn" onclick="saveRotation('${safeRKey}')" style="background: #22c55e;">${window.i18n.t('rotations.save_this')}</button>
            <span id="rot-save-status-${rotationKey}" style="font-size: 13px; color: var(--color-muted);"></span>
        </div>
    `;

    renderRotationProviders(rotationKey);
}

function renderRotationProviders(rotationKey) {
    const container = document.getElementById(`providers-${rotationKey}`);
    const rotation = rotationsConfig.rotations[rotationKey];
    
    if (!rotation.providers || rotation.providers.length === 0) {
        container.innerHTML = '<p style="color: var(--color-muted);">' + window.i18n.t('rotations.no_providers') + '</p>';
        return;
    }
    
    const safeKey = rotationKey.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
    let html = '';
    rotation.providers.forEach((provider, providerIndex) => {
        const provSelUid = `prov-sel-${rotationKey}-${providerIndex}`;

        const _pid = provider.provider_id || '';
        const _usageBadge = _providerSupportsUsage(_pid)
            ? `<span data-usage-pid="${escHtmlAttr(_pid)}" style="margin-left:6px;">${_usageCache[_pid] && _usageCache[_pid].data ? _renderUsageCompact(_usageCache[_pid].data) : ''}</span>`
            : '';
        const modelCount = provider.models ? provider.models.length : 0;

        const body = `
            <div style="display:flex; justify-content:flex-end; align-items:center; gap:6px; margin-bottom:10px;">
                ${(_pid && !META_PROVIDERS.includes(_pid)) ? `<button type="button" class="btn btn-secondary provider-toggle-btn" data-rotation="${escHtmlAttr(rotationKey)}" data-pindex="${providerIndex}" data-disabled="${provider.enabled === false ? '1' : '0'}" onclick="toggleProviderDisabled(this)" style="padding:5px 10px;font-size:12px;background:${provider.enabled === false ? '#198754' : ''};">${provider.enabled === false ? (window.i18n.t('rotations.enable') || 'Enable') : (window.i18n.t('rotations.disable') || 'Disable')}</button>` : ''}
                <button type="button" class="btn btn-secondary" onclick="removeRotationProvider('${safeKey}', ${providerIndex})" style="background: #dc3545; padding: 5px 10px; font-size: 12px;">${window.i18n.t('rotations.remove')}</button>
            </div>

            <div class="form-group">
                <label>${window.i18n.t('rotations.provider_id')}</label>
                ${buildProviderSelectHtml(provSelUid, provider.provider_id || '',
                    `function(v){updateRotationProvider('${safeKey}',${providerIndex},'provider_id',v)}`)}
            </div>

            <div class="form-group">
                <label>${window.i18n.t('rotations.weight_optional')}</label>
                <input type="number" value="${provider.weight || ''}" onchange="updateRotationProvider('${safeKey}', ${providerIndex}, 'weight', this.value ? parseInt(this.value) : null)" placeholder="${window.i18n.t('rotations.optional')}">
            </div>

            <h5 style="margin-top: 15px; margin-bottom: 10px;">${window.i18n.t('rotations.models_section')}</h5>
            <div id="models-${rotationKey}-${providerIndex}"></div>
            <button type="button" class="btn btn-secondary" onclick="addRotationModel('${safeKey}', ${providerIndex})" style="margin-top: 10px; font-size: 12px;">${window.i18n.t('rotations.add_model')}</button>
            <p style="font-size: 12px; color: var(--color-muted); margin-top: 5px;">${window.i18n.t('rotations.leave_empty')}</p>
        `;

        const titleLabel = `${window.i18n.t('rotations.provider_label')} ${providerIndex + 1}${_pid ? ' — ' + escHtmlAttr(_pid) : ''}`;
        const badge = `${modelCount} ${modelCount !== 1 ? 'models' : 'model'}${_usageBadge}`;
        html += subPanel(`rot:${rotationKey}:prov:${providerIndex}`, titleLabel, body, {badge});
    });
    container.innerHTML = html;
    rotation.providers.forEach((provider, providerIndex) => renderRotationModels(rotationKey, providerIndex));
    applyProviderToggleStates();
}

// --- Per-rotation provider disable/enable -------------------------------
// Disabling is membership in THIS rotation only: it flips `enabled` on the
// provider entry inside this rotation's config and persists it. It never touches
// the provider in other rotations or in direct API calls.

function applyProviderToggleStates() {
    document.querySelectorAll('.provider-toggle-btn').forEach(btn => {
        const rotationKey = btn.getAttribute('data-rotation');
        const pindex = parseInt(btn.getAttribute('data-pindex'), 10);
        const rot = rotationsConfig.rotations[rotationKey];
        const provider = rot && rot.providers && rot.providers[pindex];
        _setToggleBtn(btn, !!(provider && provider.enabled === false));
    });
}

function _setToggleBtn(btn, disabled) {
    btn.dataset.disabled = disabled ? '1' : '0';
    if (disabled) {
        btn.textContent = window.i18n.t('rotations.enable') || 'Enable';
        btn.style.background = '#198754';  // green = click to re-enable
    } else {
        btn.textContent = window.i18n.t('rotations.disable') || 'Disable';
        btn.style.background = '';
    }
}

// POST the current rotation config without navigating away (used by the instant
// per-entry disable toggle). Returns true on success.
async function persistRotationsSilent() {
    try {
        const orderedRotations = {};
        rotationMasterOrder.forEach(k => { if (rotationsConfig.rotations[k]) orderedRotations[k] = rotationsConfig.rotations[k]; });
        const response = await fetch(rotationsData.saveUrl, {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: 'config=' + encodeURIComponent(JSON.stringify({ ...rotationsConfig, rotations: orderedRotations }, null, 2))
        });
        return response.ok;
    } catch (e) {
        return false;
    }
}

async function toggleProviderDisabled(btn) {
    const rotationKey = btn.getAttribute('data-rotation');
    const pindex = parseInt(btn.getAttribute('data-pindex'), 10);
    const rot = rotationsConfig.rotations[rotationKey];
    if (!rot || !rot.providers || !rot.providers[pindex]) return;
    const provider = rot.providers[pindex];
    // Currently enabled (enabled !== false) → this click disables it, and vice versa.
    const willDisable = provider.enabled !== false;
    btn.disabled = true;
    // Optimistically flip, persist, revert on failure.
    provider.enabled = willDisable ? false : true;
    _setToggleBtn(btn, willDisable);
    try {
        const ok = await persistRotationsSilent();
        if (!ok) {
            provider.enabled = willDisable ? true : false;
            _setToggleBtn(btn, !willDisable);
            alert('Failed to save rotation change');
        }
    } catch (e) {
        provider.enabled = willDisable ? true : false;
        _setToggleBtn(btn, !willDisable);
        alert('Request failed: ' + e);
    } finally {
        btn.disabled = false;
    }
}

function renderRotationModels(rotationKey, providerIndex) {
    const container = document.getElementById(`models-${rotationKey}-${providerIndex}`);
    const provider = rotationsConfig.rotations[rotationKey].providers[providerIndex];

    if (!provider.models || provider.models.length === 0) {
        container.innerHTML = '<p style="color: var(--color-muted); font-size: 12px;">' + window.i18n.t('rotations.no_models') + '</p>';
        return;
    }

    let html = '';
    provider.models.forEach((model, modelIndex) => {
        const safeKey = rotationKey.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
        const inputId = `mdl-inp-${rotationKey}-${providerIndex}-${modelIndex}`;
        const providerId = provider.provider_id || '';

        // Meta-providers forward to a named rotation/autoselect: the "model name"
        // is the target id, so swap the model-search button for a datalist of
        // known rotation names and relabel the field.
        const isMeta = META_PROVIDERS.includes(providerId);
        const metaDlId = `mdl-meta-dl-${rotationKey}-${providerIndex}-${modelIndex}`;
        const metaDatalist = isMeta
            ? `<datalist id="${escHtmlAttr(metaDlId)}">${Object.keys(rotationsConfig.rotations || {}).map(rn => `<option value="${escHtmlAttr(rn)}">`).join('')}</datalist>`
            : '';
        const nameLabel = isMeta ? (window.i18n.t('rotations.target_name') || 'Target rotation/autoselect') : window.i18n.t('rotations.model_name');
        const namePlaceholder = isMeta ? (window.i18n.t('rotations.target_placeholder') || 'rotation or autoselect name…') : 'Model name…';
        const nameTrailing = isMeta
            ? metaDatalist
            : `<button type="button" class="btn btn-secondary"
                    data-provider="${escHtmlAttr(providerId)}"
                    data-input-id="${escHtmlAttr(inputId)}"
                    onclick="msOpenProviderForInput(this)"
                    style="padding:4px 8px;font-size:11px;white-space:nowrap;">${window.i18n.t('rotations.search')}</button>`;

        const advancedInner = `
            <div class="form-group" style="margin-bottom: 8px;">
                <label style="font-size: 12px;">${window.i18n.t('rotations.max_request_tokens')}</label>
                <input type="number" value="${model.max_request_tokens || ''}" onchange="updateRotationModel('${rotationKey}', ${providerIndex}, ${modelIndex}, 'max_request_tokens', this.value ? parseInt(this.value) : null)" placeholder="${window.i18n.t('rotations.optional')}" style="font-size: 12px; padding: 5px;">
            </div>

            <div class="form-group" style="margin-bottom: 8px;">
                <label style="font-size: 12px;">${window.i18n.t('rotations.context_size')}</label>
                <input type="number" value="${model.context_size || ''}" onchange="updateRotationModel('${rotationKey}', ${providerIndex}, ${modelIndex}, 'context_size', this.value ? parseInt(this.value) : null)" placeholder="${window.i18n.t('rotations.optional')}" style="font-size: 12px; padding: 5px;">
            </div>

            <div class="form-group" style="margin-bottom: 8px;">
                <label style="font-size: 12px;">${window.i18n.t('rotations.condense_context_pct')}</label>
                <input type="number" value="${model.condense_context || ''}" onchange="updateRotationModel('${rotationKey}', ${providerIndex}, ${modelIndex}, 'condense_context', this.value ? parseInt(this.value) : null)" placeholder="${window.i18n.t('rotations.optional')} (default: 80)" style="font-size: 12px; padding: 5px;">
            </div>

            <div class="form-group" style="margin-bottom: 8px;">
                <label style="font-size: 12px;">${window.i18n.t('rotations.condense_method')}</label>
                <input type="text" value="${Array.isArray(model.condense_method) ? model.condense_method.join(', ') : (model.condense_method || '')}" onchange="updateRotationModelCondenseMethod('${rotationKey}', ${providerIndex}, ${modelIndex}, this.value)" placeholder="${window.i18n.t('rotations.condense_method_placeholder')}" style="font-size: 12px; padding: 5px;">
            </div>

            <div style="margin-top:10px;padding-top:10px;border-top:1px solid var(--color-border);">
                ${triStateCompact('Context Condensation', model.enable_context_condensation, `updateRotationModelTriState('${rotationKey}', ${providerIndex}, ${modelIndex}, 'enable_context_condensation', this.value)`)}
                ${triStateCompact('Response Cache', model.enable_response_cache, `updateRotationModelTriState('${rotationKey}', ${providerIndex}, ${modelIndex}, 'enable_response_cache', this.value)`)}
                ${triStateCompact('Prompt Batching', model.enable_prompt_batching, `updateRotationModelTriState('${rotationKey}', ${providerIndex}, ${modelIndex}, 'enable_prompt_batching', this.value)`)}
                ${triStateCompact('Prompt Security', model.enable_prompt_security, `updateRotationModelTriState('${rotationKey}', ${providerIndex}, ${modelIndex}, 'enable_prompt_security', this.value)`)}
                ${triStateCompact('Context Lens Analytics', model.enable_context_lens, `updateRotationModelTriState('${rotationKey}', ${providerIndex}, ${modelIndex}, 'enable_context_lens', this.value)`)}
                ${triStateCompact('Block High-Risk Prompts', model.block_high_risk_prompts, `updateRotationModelTriState('${rotationKey}', ${providerIndex}, ${modelIndex}, 'block_high_risk_prompts', this.value)`)}
                ${triStateCompact('NSFW Classification', model.enable_nsfw_classification, `updateRotationModelTriState('${rotationKey}', ${providerIndex}, ${modelIndex}, 'enable_nsfw_classification', this.value)`)}
                ${triStateCompact('Privacy Classification', model.enable_privacy_classification, `updateRotationModelTriState('${rotationKey}', ${providerIndex}, ${modelIndex}, 'enable_privacy_classification', this.value)`)}
            </div>
        `;

        const modelBody = `
            <div style="display: flex; justify-content: flex-end; align-items: center; margin-bottom: 8px;">
                <button type="button" onclick="removeRotationModel('${safeKey}', ${providerIndex}, ${modelIndex})" style="background: #dc3545; color: white; border: none; padding: 3px 8px; border-radius: 3px; cursor: pointer; font-size: 11px;">${window.i18n.t('rotations.remove')}</button>
            </div>

            <div class="form-group" style="margin-bottom: 8px;">
                <label style="font-size: 12px;">${nameLabel}</label>
                <div style="display:flex;gap:5px;align-items:center;">
                    <input type="text" id="${escHtmlAttr(inputId)}" value="${escHtmlAttr(model.name)}"
                        ${isMeta ? `list="${escHtmlAttr(metaDlId)}"` : ''}
                        oninput="updateRotationModel('${safeKey}',${providerIndex},${modelIndex},'name',this.value)"
                        onchange="updateRotationModel('${safeKey}',${providerIndex},${modelIndex},'name',this.value)"
                        placeholder="${escHtmlAttr(namePlaceholder)}"
                        required style="flex:1;font-size:12px;padding:5px;">
                    ${nameTrailing}
                </div>
            </div>

            <div class="form-group" style="margin-bottom: 8px;">
                <label style="font-size: 12px;">${window.i18n.t('rotations.weight')}</label>
                <input type="number" value="${model.weight || 1}" onchange="updateRotationModel('${rotationKey}', ${providerIndex}, ${modelIndex}, 'weight', parseInt(this.value))" style="font-size: 12px; padding: 5px;">
            </div>

            <div class="form-group" style="margin-bottom: 8px;">
                <label style="font-size: 12px;">${window.i18n.t('rotations.rate_limit')}</label>
                <input type="number" value="${model.rate_limit || 0}" onchange="updateRotationModel('${rotationKey}', ${providerIndex}, ${modelIndex}, 'rate_limit', parseFloat(this.value))" step="0.1" style="font-size: 12px; padding: 5px;">
            </div>

            ${subPanel(`rot:${rotationKey}:prov:${providerIndex}:mdl:${modelIndex}:adv`, window.i18n.t('rotations.advanced') || 'Advanced', advancedInner, {nested: true})}
        `;

        const mTitle = `${window.i18n.t('rotations.model_label')} ${modelIndex + 1}${model.name ? ' — ' + escHtmlAttr(model.name) : ''}`;
        html += subPanel(`rot:${rotationKey}:prov:${providerIndex}:mdl:${modelIndex}`, mTitle, modelBody, {nested: true});
    });
    container.innerHTML = html;
}

function updateGlobalNotify(value) {
    rotationsConfig.notifyerrors = value;
}

async function apiCall(method, url, body) {
    const opts = { method, headers: { 'Content-Type': 'application/json' } };
    if (body !== undefined) opts.body = JSON.stringify(body);
    const r = await fetch(url, opts);
    return r.json();
}

function triStateValue(value) {
    if (value === true || value === 'enabled') return 'enabled';
    if (value === false || value === 'disabled') return 'disabled';
    return 'inherit';
}

function triStateCompact(label, value, onChange) {
    return `
        <div class="form-group" style="margin-bottom: 8px;">
            <label style="font-size: 12px;">${label}</label>
            <select onchange="${onChange}" style="font-size: 12px; padding: 5px;">
                <option value="inherit" ${triStateValue(value) === 'inherit' ? 'selected' : ''}>Inherit</option>
                <option value="enabled" ${triStateValue(value) === 'enabled' ? 'selected' : ''}>Enabled</option>
                <option value="disabled" ${triStateValue(value) === 'disabled' ? 'selected' : ''}>Disabled</option>
            </select>
        </div>
    `;
}

function updateRotationTriState(rotationKey, field, value) {
    rotationsConfig.rotations[rotationKey][field] = value === 'inherit' ? null : value === 'enabled';
}

function updateRotationModelTriState(rotationKey, providerIndex, modelIndex, field, value) {
    rotationsConfig.rotations[rotationKey].providers[providerIndex].models[modelIndex][field] = value === 'inherit' ? null : value === 'enabled';
}

async function saveRotation(key) {
    const statusEl = document.getElementById(`rot-save-status-${key}`);
    if (statusEl) statusEl.textContent = window.i18n.t('rotations.saving');
    try {
        const result = await apiCall('POST', `${BASE_PATH}/dashboard/api/rotation`, {
            rotation_id: key,
            config: rotationsConfig.rotations[key]
        });
        if (result.success) {
            if (statusEl) { statusEl.textContent = window.i18n.t('rotations.saved'); statusEl.style.color = '#4ade80'; setTimeout(() => { statusEl.textContent = ''; }, 3000); }
        } else {
            if (statusEl) { statusEl.textContent = 'Error: ' + (result.error || 'Unknown'); statusEl.style.color = '#f87171'; }
        }
    } catch (e) {
        if (statusEl) { statusEl.textContent = 'Error: ' + e.message; statusEl.style.color = '#f87171'; }
    }
}

async function copyRotation(sourceKey) {
    const newKey = await showPrompt(window.i18n.interpolate(window.i18n.t('rotations.copy_prompt'), {key: sourceKey}), `${sourceKey}_copy`, 'rotation-key', window.i18n.t('rotations.copy_title'));
    if (!newKey) return;
    if (newKey === sourceKey) { showAlert(window.i18n.t('rotations.key_different'), window.i18n.t('rotations.invalid_key_title'), '⚠️', 'warn'); return; }
    if (rotationsConfig.rotations[newKey]) { showAlert(window.i18n.t('rotations.key_exists'), window.i18n.t('rotations.key_exists_title'), '⚠️', 'warn'); return; }
    const cloned = JSON.parse(JSON.stringify(rotationsConfig.rotations[sourceKey]));
    cloned.model_name = newKey;
    rotationsConfig.rotations[newKey] = cloned;
    try {
        const result = await apiCall('POST', `${BASE_PATH}/dashboard/api/rotation`, { rotation_id: newKey, config: cloned });
        if (!result.success) {
            showAlert('Error copying rotation: ' + (result.error || 'Unknown'), 'Error', '❌', 'danger');
            delete rotationsConfig.rotations[newKey];
            return;
        }
    } catch (e) {
        showAlert('Error copying rotation: ' + e.message, 'Error', '❌', 'danger');
        delete rotationsConfig.rotations[newKey];
        return;
    }
    if (!rotationMasterOrder.includes(newKey)) rotationMasterOrder.push(newKey);
    expandedRotations.add(newKey);
    renderRotationsList();
}

async function addRotation() {
    const key = await showPrompt(window.i18n.t('rotations.add_prompt'), '', 'rotation-key', window.i18n.t('rotations.add_title'));
    if (!key) return;
    if (rotationsConfig.rotations[key]) {
        showAlert(window.i18n.t('rotations.key_exists'), window.i18n.t('rotations.key_exists_title'), '⚠️', 'warn');
        return;
    }
    if (!rotationsConfig.rotations) rotationsConfig.rotations = {};
    const newCfg = { model_name: key, notifyerrors: false, capabilities: [], providers: [] };
    rotationsConfig.rotations[key] = newCfg;
    try {
        const result = await apiCall('POST', `${BASE_PATH}/dashboard/api/rotation`, { rotation_id: key, config: newCfg });
        if (!result.success) {
            showAlert('Error creating rotation: ' + (result.error || 'Unknown'), 'Error', '❌', 'danger');
            delete rotationsConfig.rotations[key];
            return;
        }
    } catch (e) {
        showAlert('Error creating rotation: ' + e.message, 'Error', '❌', 'danger');
        delete rotationsConfig.rotations[key];
        return;
    }
    if (!rotationMasterOrder.includes(key)) rotationMasterOrder.push(key);
    expandedRotations.add(key);
    renderRotationsList();
}

async function removeRotation(key) {
    if (await showDangerConfirm(window.i18n.interpolate(window.i18n.t('rotations.remove_confirm'), {key}), window.i18n.t('rotations.remove_title'))) {
        try {
            const result = await apiCall('DELETE', `${BASE_PATH}/dashboard/api/rotation/${encodeURIComponent(key)}`);
            if (!result.success) { showAlert('Error: ' + (result.error || 'Unknown'), 'Error', '❌', 'danger'); return; }
        } catch (e) { showAlert('Error: ' + e.message, 'Error', '❌', 'danger'); return; }
        delete rotationsConfig.rotations[key];
        rotationMasterOrder = rotationMasterOrder.filter(k => k !== key);
        expandedRotations.delete(key);
        renderRotationsList();
    }
}

function updateRotation(key, field, value) {
    rotationsConfig.rotations[key][field] = value;
}

function updateRotationCapabilities(rotationKey, value) {
    const trimmed = value.trim();
    if (!trimmed) {
        rotationsConfig.rotations[rotationKey].capabilities = null;
        return;
    }
    
    // Split by comma and clean up
    rotationsConfig.rotations[rotationKey].capabilities = 
        trimmed.split(',').map(s => s.trim()).filter(s => s);
}

function addRotationProvider(rotationKey) {
    if (!rotationsConfig.rotations[rotationKey].providers) {
        rotationsConfig.rotations[rotationKey].providers = [];
    }
    
    rotationsConfig.rotations[rotationKey].providers.push({
        provider_id: '',
        models: []
    });
    
    renderRotationProviders(rotationKey);
}

async function removeRotationProvider(rotationKey, providerIndex) {
    if (await showDangerConfirm(window.i18n.t('rotations.remove_provider_confirm'), window.i18n.t('rotations.remove_provider_title'))) {
        rotationsConfig.rotations[rotationKey].providers.splice(providerIndex, 1);
        renderRotationProviders(rotationKey);
    }
}

function updateRotationProvider(rotationKey, providerIndex, field, value) {
    rotationsConfig.rotations[rotationKey].providers[providerIndex][field] = value;
}

function addRotationModel(rotationKey, providerIndex) {
    const provider = rotationsConfig.rotations[rotationKey].providers[providerIndex];
    if (!provider.models) {
        provider.models = [];
    }
    
    provider.models.push({
        name: '',
        weight: 1,
        rate_limit: 0
    });
    
    renderRotationModels(rotationKey, providerIndex);
}

async function removeRotationModel(rotationKey, providerIndex, modelIndex) {
    if (await showDangerConfirm(window.i18n.t('rotations.remove_model_confirm'), window.i18n.t('rotations.remove_model_title'))) {
        rotationsConfig.rotations[rotationKey].providers[providerIndex].models.splice(modelIndex, 1);
        renderRotationModels(rotationKey, providerIndex);
    }
}

function updateRotationModel(rotationKey, providerIndex, modelIndex, field, value) {
    rotationsConfig.rotations[rotationKey].providers[providerIndex].models[modelIndex][field] = value;
}

function updateRotationModelCondenseMethod(rotationKey, providerIndex, modelIndex, value) {
    const trimmed = value.trim();
    if (!trimmed) {
        rotationsConfig.rotations[rotationKey].providers[providerIndex].models[modelIndex].condense_method = null;
        return;
    }
    
    // Check if it's a comma-separated list
    if (trimmed.includes(',')) {
        rotationsConfig.rotations[rotationKey].providers[providerIndex].models[modelIndex].condense_method = 
            trimmed.split(',').map(s => s.trim()).filter(s => s);
    } else {
        rotationsConfig.rotations[rotationKey].providers[providerIndex].models[modelIndex].condense_method = trimmed;
    }
}

async function saveRotations() {
    try {
        const response = await fetch(rotationsData.saveUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: (function() {
                const orderedRotations = {};
                rotationMasterOrder.forEach(k => { if (rotationsConfig.rotations[k]) orderedRotations[k] = rotationsConfig.rotations[k]; });
                return 'config=' + encodeURIComponent(JSON.stringify({ ...rotationsConfig, rotations: orderedRotations }, null, 2));
            })()
        });
        
        if (response.ok) {
            window.location.href = rotationsData.successUrl;
        } else {
            showAlert(window.i18n.t('rotations.error_saving'), 'Error', '❌', 'danger');
        }
    } catch (error) {
        showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
    }
}

// Initial render — defer until i18n is ready so translated strings are available
function _doInitialRender() { renderRotationsList(); }
if (window.i18n) { _doInitialRender(); } else { document.addEventListener('i18n:ready', _doInitialRender, {once: true}); }
</script>

<style>
.rotation-item {
    animation: fadeIn 0.3s;
}

.rotation-header:hover {
    background: var(--bg-accent);
}

@keyframes fadeIn {
    from { opacity: 0; transform: translateY(-10px); }
    to { opacity: 1; transform: translateY(0); }
}

.form-group {
    margin-bottom: 15px;
}

.form-group label {
    display: block;
    margin-bottom: 5px;
    font-weight: 500;
    color: var(--color-text);
}

.form-group input[type="text"],
.form-group input[type="number"],
.form-group select {
    width: 100%;
    padding: 8px;
    border: 1px solid var(--color-border);
    border-radius: 3px;
    font-size: 14px;
    background: var(--bg-page);
    color: var(--color-text);
}

.form-group input[type="checkbox"] {
    margin-right: 5px;
}
</style>
{% endblock %}
