/* Views contain presentation only; mutations live in app.js. */ (function (M) { 'use strict'; const { escape: E, tr: T, t, icon: I, button: B, iconButton: IB, card: C, field: F, select: S } = M; M.views = {}; M.supportsExport = router => router?.device_type !== 'switchos' && router?.supports_export !== false; M.isTextExport = file => !!file && file.backup_type === 'export' && M.supportsExport(file); M.deviceUsername = router => Object.hasOwn(router, 'effective_username') ? router.effective_username || '—' : router.ssh_user || '—'; const typeTag = type => `${T(type === 'export' ? 'files.exportType' : 'files.binaryType')}`; const metric = (key, value, note, small = false) => `

${T(key)}

${E(value)}
${note}
`; const dl = rows => `
${rows.map(([label, value, html]) => `
${T(label)}
${html ? value : E(value == null || value === '' ? '—' : value)}
`).join('')}
`; const backupActions = file => `${M.isTextExport(file) ? IB('preview', 'common.preview', 'eye', `data-id="${file.id}"`) + IB('diff-latest', 'files.latestForRouter', 'diff', `data-id="${file.id}"`) : ''}${IB('download', 'common.download', 'download', `data-id="${file.id}"`)}${IB('email', 'common.email', 'mail', `data-id="${file.id}"`)}${file.device_type !== 'switchos' && file.backup_type === 'binary' ? IB('upload', 'ui.upload', 'upload', `data-id="${file.id}" data-router="${file.router_id}"`) : ''}${IB('delete-backup', 'common.delete', 'trash', `data-id="${file.id}"`, 'danger')}`; M.backupTable = (files, options = {}) => { const { compact = false, select = false, paginate = true } = options; if (!files.length) return M.empty('ui.noBackups', '', 'files'); const visible = paginate ? M.paged(files) : files; return `
${select ? `` : ''}${compact ? '' : ``}${visible.map(f => `${select ? `` : ''}${compact ? '' : ``}`).join('')}
M.state.selected.has(f.id)) ? ' checked' : ''}>${T(compact ? 'files.routerColumn' : 'files.fileColumn')}${T('files.typeColumn')}${T('files.routerColumn')}${T('files.sizeColumn')}${T('files.createdColumn')}${T('common.actions')}
${compact ? `${E(f.router_name || `#${f.router_id}`)}` : `${E(f.file_name)}${E(f.checksum ? f.checksum.slice(0, 12) + '…' : '—')}`}${typeTag(f.backup_type)}${E(f.router_name || `#${f.router_id}`)}${E(f.device_type === 'switchos' ? 'SwitchOS' : 'RouterOS')}${E(M.bytes(f.file_size))}${E(M.age(f.created_at))}${compact ? '' : `${E(M.date(f.created_at))}`}
${compact ? IB('download', 'common.download', 'download', `data-id="${f.id}"`) : backupActions(f)}
${paginate ? M.pagination(files.length) : ''}`; }; M.views.dashboard = async () => { const revision = M.state.routeId; const [d, backups, routers] = await Promise.all([M.api('/dashboard'), M.api('/backups'), M.api('/routers')]); if (revision !== M.state.routeId) return ""; M.state.cache.dashboard = d; M.state.cache.backups = backups; M.state.cache.routers = routers; const sorted = backups.slice().sort((a, b) => (M.parseDate(b.created_at)?.getTime() || 0) - (M.parseDate(a.created_at)?.getTime() || 0)); const last = sorted[0], online = routers.filter(r => r.last_connection_status === true || r.last_connection_status === 1).length; const offline = routers.filter(r => r.last_connection_status === false || r.last_connection_status === 0).length; const unknown = routers.length - online - offline, total = routers.length || 1, circle = 326.73; const storage = d.storage || {}, totalBytes = Number(storage.total) || 0; const capacity = (key, value, tone = '') => `
${T(key)}${E(M.bytes(value))}
`; const days = Array.from({ length: 7 }, (_, i) => { const date = new Date(); date.setHours(0, 0, 0, 0); date.setDate(date.getDate() - 6 + i); const next = new Date(date); next.setDate(next.getDate() + 1); return { label: date.toLocaleDateString(M.state.lang, { day: '2-digit', month: '2-digit' }), count: backups.filter(f => { const dt = M.parseDate(f.created_at); return dt && dt >= date && dt < next; }).length }; }); const maxDay = Math.max(1, ...days.map(day => day.count)); const activity = `
`; const status = `
${routers.length}${T('nav.routers')}
${[['ui.online', online, 'success'], ['ui.offline', offline, 'danger'], ['ui.unknown', unknown, 'muted']].map(([key, n, tone]) => `
${M.badge(t(key), tone)}${n}
`).join('')}
${T('ui.lastKnown')}
`; const logs = d.recent_logs || []; const logList = logs.length ? `
${logs.slice(0, 5).map(log => `
${I('clock')}

${E(log.message)}

${E(M.date(log.timestamp))}
`).join('')}
` : M.empty('ui.noLogs', '', 'clock'); const counters = new Map(); for (const file of backups) { const row = counters.get(file.router_id) || { name: file.router_name || `#${file.router_id}`, count: 0 }; row.count++; counters.set(file.router_id, row); } const topDevices = [...counters.values()].sort((a, b) => b.count - a.count).slice(0, 5); const maxBackups = Math.max(1, ...topDevices.map(row => row.count)); const today = new Date().toDateString(); const summary = dl([ ['dashboard.avgBackupsPerRouter', routers.length ? (backups.length / routers.length).toFixed(1) : '0'], ['dashboard.coverageLabel', `${routers.length ? Math.round(counters.size / routers.length * 100) : 0}%`], ['dashboard.weeklyActivityLabel', backups.filter(file => (M.parseDate(file.created_at)?.getTime() || 0) >= Date.now() - 7 * 86400000).length], ['dashboard.exportShareLabel', `${backups.length ? Math.round(Number(d.export_count) / backups.length * 100) : 0}%`], ['dashboard.activityTodayLabel', logs.filter(log => M.parseDate(log.timestamp)?.toDateString() === today).length], ['dashboard.busiestRouterLabel', topDevices[0] ? `${topDevices[0].name} (${topDevices[0].count})` : '—'] ]); const topChart = topDevices.length ? `
${topDevices.map(row => `
${E(row.name)}${row.count}
`).join('')}
` : M.empty('ui.noBackups'); const actions = B('new-backup', t('ui.newBackup'), 'play', '', 'primary') + B('new-device', t('ui.newDevice'), 'plus'); return `${M.heading('dashboard.title', 'ui.loginHint', actions, 'dashboard.eyebrow')}
${metric('nav.routers', d.routers_count, `${online} ${T('ui.online')}${offline} ${T('ui.offline')}`)}${metric('ui.lastBackup', last ? M.age(last.created_at) : '—', E(last?.router_name || '—'), true)}${metric('dashboard.folderUsage', M.bytes(storage.folder_used), `${T('dashboard.freeSpace')}: ${E(M.bytes(storage.free))}`, true)}${metric('ui.backups', d.total_backups, `${Number(d.export_count) || 0} RSC / ${Number(d.binary_count) || 0} binary`)}
${C('ui.recentBackups', M.backupTable(sorted.slice(0, 6), { compact: true, paginate: false }), `${T('ui.viewAll')} →`)}${C('ui.activity', activity)}${C('dashboard.activityTitle', logList, `${T('ui.viewAll')} →`)}
${C('ui.devicesStatus', status)}${C('ui.quickActions', `
${routers.some(M.supportsExport) ? B('batch-export', t('ui.exportAll'), 'files') : ''}${B('batch-binary', t('ui.binaryAll'), 'database')}
`)}${C('dashboard.storageTitle', `
${capacity('dashboard.totalDisk', storage.total)}${capacity('dashboard.usedSpace', storage.used, 'warning')}${capacity('dashboard.freeSpace', storage.free, 'success')}${capacity('dashboard.folderUsage', storage.folder_used)}
`)}${C('dashboard.storageViewMix', `
${T('files.exportType')}${d.export_count}
${T('files.binaryType')}${d.binary_count}
`)}
${C('dashboard.storageSnapshotTitle', `
${summary}
`)}${C('dashboard.storageViewRouters', topChart)}
`; }; M.filteredDevices = () => { const f = M.state.deviceFilters || {}; return (M.state.cache.routers || []).filter(r => { const match = `${r.name} ${r.host} ${r.last_connection_model || ''}`.toLocaleLowerCase().includes((f.search || '').toLocaleLowerCase()); const status = r.last_connection_status == null ? 'unknown' : r.last_connection_status ? 'online' : 'offline'; return match && (!f.device_type || f.device_type === r.device_type) && (!f.status || f.status === status); }); }; M.deviceTable = () => { const rows = M.filteredDevices(); if (!rows.length) return M.empty((M.state.cache.routers || []).length ? 'ui.noMatches' : 'ui.empty', (M.state.cache.routers || []).length ? '' : 'ui.emptyHint', 'device'); return `
${M.paged(rows).map(r => ``).join('')}
${T('routers.name')}${T('ui.address')}${T('routers.model')}${T('ui.status')}${T('routers.ping')}${T('routers.lastTestAt')}${T('routers.backupPolicy')}
${E(r.name)}${E(r.device_type === 'switchos' ? 'SwitchOS' : 'RouterOS')}${E(r.host)}${r.port} · ${E(M.deviceUsername(r))}${E(r.last_connection_model || '—')}${E(r.last_connection_version || '')}${M.connectionBadge(r)}${r.disable_ping ? `${T('routers.pingDisabled')}` : `—`}${E(r.last_connection_tested_at ? M.age(r.last_connection_tested_at) : '—')}${r.disable_all_backups ? M.badge(t('common.disabled'), 'warning') : `${!r.disable_export_backups && r.device_type !== 'switchos' ? 'RSC' : '—'} / ${!r.disable_binary_backups ? 'BIN' : '—'}`}
${IB('test-device', 'routers.testConnection', 'refresh', `data-id="${r.id}"`)}${IB('edit-device', 'common.edit', 'edit', `data-id="${r.id}"`)}${I('chevron')}
${M.pagination(rows.length)}`; }; M.views.devices = async () => { const revision = M.state.routeId; const routers = await M.api('/routers'); if (revision !== M.state.routeId) return ''; M.state.cache.routers = routers; const f = M.state.deviceFilters || {}; return `${M.heading('routers.title', 'routers.subtitle', B('new-device', t('ui.newDevice'), 'plus', '', 'primary') + B('refresh', t('ui.refresh'), 'refresh'), 'routers.eyebrow')}
${T('files.searchLabel')}
${S('status', 'ui.status', [['', t('ui.allStatuses')], ['online', t('ui.online')], ['offline', t('ui.offline')], ['unknown', t('ui.unknown')]], f.status || '')}${S('device_type', 'routers.deviceType', [['', t('files.allTypes')], ['routeros', 'RouterOS'], ['switchos', 'SwitchOS']], f.device_type || '')}${B('refresh-ping', t('ui.refreshPing'), 'refresh')}
${M.deviceTable()}
`; }; M.deviceForm = router => { const editing = !!router, r = router || { name: '', host: '', port: 22, device_type: 'routeros', ssh_user: 'admin' }; const switchos = r.device_type === 'switchos'; const legacy = editing && switchos && r.use_global_switchos_credentials == null; const source = legacy ? '' : r.use_global_switchos_credentials === false ? 'false' : 'true'; const global = switchos && source === 'true'; const passwordSet = r.has_own_password ?? (!r.uses_global_switchos_credentials && r.has_effective_password); const sources = [...(legacy ? [['', t('ui.legacyCredentialSource')]] : []), ['true', t('ui.globalCredentials')], ['false', t('ui.deviceCredentials')]]; return `
${F('name', 'routers.name', r.name, { required: true, attrs: 'pattern="[A-Za-z0-9_-]+" maxlength="120" autocomplete="off"', hint: t('routers.nameValidationHint') })} ${S('device_type', 'routers.deviceType', [['routeros', 'RouterOS'], ['switchos', 'SwitchOS']], r.device_type)} ${F('host', 'routers.host', r.host, { required: true, attrs: 'maxlength="255" autocomplete="off"', placeholder: '192.168.88.1' })} ${F('port', 'routers.port', r.port, { type: 'number', required: true, attrs: 'min="1" max="65535"' })}
${S('use_global_switchos_credentials', 'ui.credentialSource', sources, source, switchos ? '' : 'disabled')}

${T(legacy ? 'ui.legacyCredentialsHint' : 'ui.globalCredentialsHint')}

${F('ssh_user', 'routers.sshUser', r.ssh_user || '', { attrs: 'maxlength="120" autocomplete="off"' + (global ? ' disabled' : '') })}
${F('ssh_password', 'routers.sshPassword', '', { type: 'password', attrs: 'maxlength="255" autocomplete="new-password"' + (global ? ' disabled' : ''), placeholder: editing ? t(passwordSet ? 'ui.kept' : 'ui.notSet') : '' })}${editing ? `` : ''}
${F('ssh_key', 'routers.sshPrivateKey', '', { type: 'textarea', attrs: 'spellcheck="false" autocomplete="off"' + (switchos ? ' disabled' : ''), placeholder: editing && r.has_effective_ssh_key ? t('ui.kept') : t('routers.optionalPrivateKey'), hint: r.uses_global_ssh_key ? t('routers.globalKeyMode') : '' })}${editing ? `` : ''}
${B('close-dialog', t('common.cancel'))}
`; }; M.detailTabs = () => `
${[['overview', 'ui.overview'], ['export', 'routers.exportsLabel'], ['binary', 'routers.binaryLabel'], ['policy', 'routers.backupSettingsTitle']].filter(([key]) => key !== 'export' || M.supportsExport(M.state.cache.device)).map(([key, label]) => ``).join('')}
`; M.detailBody = () => { const r = M.state.cache.device, backups = M.state.cache.deviceBackups || []; if (!r) return ''; if (M.state.detailTab === 'export' && !M.supportsExport(r)) M.state.detailTab = 'overview'; if (M.state.detailTab === 'export' || M.state.detailTab === 'binary') return C(M.state.detailTab === 'export' ? 'routers.exportsTableTitle' : 'routers.binaryTableTitle', M.backupTable(backups.filter(f => f.backup_type === M.state.detailTab), { select: true }), `
${B('bulk-download', t('files.downloadZip'), 'download')}${M.state.detailTab === 'export' ? B('compare-selected', t('ui.compare'), 'diff', 'hidden') : ''}
`); if (M.state.detailTab === 'policy') return C('routers.backupSettingsTitle', `

${T('routers.backupSettingsHint')}

${M.toggle('disable_all_backups', 'routers.disableAllBackups', r.disable_all_backups, 'routers.disableAllBackupsHint')}${M.supportsExport(r) ? M.toggle('disable_export_backups', 'routers.disableExports', r.disable_export_backups, 'routers.disableExportsHint') : ''}${M.toggle('disable_binary_backups', 'routers.disableBinaryBackups', r.disable_binary_backups, 'routers.disableBinaryBackupsHint')}${M.toggle('disable_ping', 'routers.disablePing', r.disable_ping, 'routers.disablePingHint')}
`, '', 'policy-card'); const summary = dl([['routers.name', r.name], ['routers.host', `${r.host}:${r.port}`], ['routers.deviceType', r.device_type === 'switchos' ? 'SwitchOS' : 'RouterOS'], ['routers.sshUser', M.deviceUsername(r)], ['routers.hostname', r.last_connection_hostname], ['routers.model', r.last_connection_model], ['routers.version', r.last_connection_version], ['ui.created', M.date(r.created_at)]]); const status = dl([['ui.status', M.connectionBadge(r), true], ['routers.lastTestAt', M.date(r.last_connection_tested_at)], ['routers.uptime', r.last_connection_uptime], ['routers.connectionLabel', r.last_connection_transport], ['routers.serverHeader', r.last_connection_server], ['routers.authMode', r.last_connection_auth_mode], ['routers.httpStatus', r.last_connection_http_status], ['routers.backupEndpoint', r.last_connection_backup_available == null ? '—' : t(r.last_connection_backup_available ? 'routers.backupAvailable' : 'routers.backupUnavailable')]]); const last = backups[0]; const backupSummary = dl([['ui.lastBackup', last ? M.date(last.created_at) : '—'], ['files.sizeColumn', last ? M.bytes(last.file_size) : '—'], ...(M.supportsExport(r) ? [['routers.exportsLabel', backups.filter(M.isTextExport).length]] : []), ['routers.binaryLabel', backups.filter(f => f.backup_type === 'binary').length]]); return `
${C('routers.connectionSectionTitle', `
${summary}
`)}${C('ui.backups', `
${backupSummary}
${M.supportsExport(r) ? B('device-export', t('files.exportType') + ' RSC', 'files', `data-id="${r.id}" ${r.disable_all_backups || r.disable_export_backups ? 'disabled' : ''}`) : ''}${B('device-binary', t('files.binaryType'), 'database', `data-id="${r.id}" ${r.disable_all_backups || r.disable_binary_backups ? 'disabled' : ''}`, 'primary')}
`)}
${C('routers.deviceStatusTitle', `
${status}${r.last_connection_error ? `
${I('alert')}${E(r.last_connection_error)}
` : ''}${!r.last_connection_tested_at ? `

${T('routers.noConnection')}

` : ''}
`)}${C('routers.credentialsSectionTitle', `
${dl([['routers.sshKey', r.uses_global_ssh_key ? t('routers.globalKeyMode') : t(r.has_effective_ssh_key ? 'routers.keyMode' : 'routers.noKey')], ['routers.sshPassword', t(r.has_effective_password ? 'routers.passwordMode' : 'routers.noPassword')], ['routers.defaultCredentials', t(r.uses_global_switchos_credentials ? 'common.enabled' : 'common.disabled')]])}
`)}
`; }; M.views.device = async id => { const revision = M.state.routeId; const [r, backups] = await Promise.all([M.api(`/routers/${id}`), M.api(`/backups/router/${id}`)]); if (revision !== M.state.routeId) return ""; if (M.state.cache.device?.id !== r.id) M.state.detailTab = 'overview'; M.state.detailTab ||= 'overview'; M.state.cache.device = r; M.state.cache.deviceBackups = backups; if (M.state.detailTab === 'export' && !M.supportsExport(r)) M.state.detailTab = 'overview'; const actions = B('test-device', t('routers.testConnection'), 'refresh', `data-id="${r.id}"`) + B('edit-device', t('common.edit'), 'edit', `data-id="${r.id}"`) + IB('delete-device', 'routers.deleteRouter', 'trash', `data-id="${r.id}"`, 'danger'); return `

${T('nav.routers')} / ${T('routers.detailTitle')}

${E(r.name)}

${E(r.host)} · ${E(r.last_connection_model || (r.device_type === 'switchos' ? 'SwitchOS' : 'RouterOS'))}

${M.connectionBadge(r)}
${actions}
${M.detailTabs(r.id)}
${M.detailBody()}
`; }; M.backupQuery = filters => { const params = new URLSearchParams(); Object.entries(filters).forEach(([key, value]) => { if (value !== '' && value !== null && value !== undefined) params.set(key, String(value)); }); return params.toString(); }; M.fileToolbar = () => `
${B('bulk-download', t('files.downloadZip'), 'download')}${B('compare-selected', t('ui.compare'), 'diff', 'hidden')}${B('bulk-delete', t('common.delete'), 'trash', '', 'danger')}${T('ui.selection')}: ${M.state.selected.size}
`; M.views.files = async () => { const revision = M.state.routeId; const f = M.state.filters; const [files, routers] = await Promise.all([M.api(`/backups?${M.backupQuery(f)}`), M.api('/routers')]); if (revision !== M.state.routeId) return ""; M.state.cache.files = files; M.state.cache.routers = routers; M.state.selected = new Set([...M.state.selected].filter(id => files.some(file => file.id === id))); return `${M.heading('ui.backups', 'files.subtitle', B('new-backup', t('ui.newBackup'), 'plus', '', 'primary') + B('refresh', t('ui.refresh'), 'refresh'), 'files.eyebrow')}
${T('files.searchLabel')}
${S('router_id', 'files.routerLabel', [['', t('files.allRouters')], ...routers.map(r => [r.id, r.name])], f.router_id)}${S('backup_type', 'files.typeLabel', [['', t('files.allTypes')], ['export', t('files.exportType')], ['binary', t('files.binaryType')]], f.backup_type)}${F('created_on', 'files.dateLabel', f.created_on, { type: 'date' })}${S('sort_by', 'files.sortLabel', [['created_at', t('files.sortNewest')], ['file_name', t('files.sortName')], ['router_name', t('files.sortRouter')], ['backup_type', t('files.sortType')]], f.sort_by)}${S('order', 'files.orderLabel', [['desc', t('common.desc')], ['asc', t('common.asc')]], f.order)}${IB('reset-filters', 'common.reset', 'close')}
${M.fileToolbar()}
${M.backupTable(files, { select: true })}
`; }; M.highlight = (text, term) => { text = String(text || ''); term = String(term || ''); if (!term) return E(text); let result = '', start = 0, at; const lower = text.toLocaleLowerCase(), query = term.toLocaleLowerCase(); while ((at = lower.indexOf(query, start)) >= 0) { result += E(text.slice(start, at)) + '' + E(text.slice(at, at + term.length)) + ''; start = at + term.length; } return result + E(text.slice(start)); }; M.smartDiff = smart => { smart = smart || {}; const sections = Array.isArray(smart.sections) ? smart.sections : []; const total = Number(smart.semantic_changes) || 0; const attention = Number(smart.attention_sections) || 0; const detail = change => { const fields = Array.isArray(change.fields) ? change.fields : []; const fieldText = fields.length ? `
${fields.map(field => `${E(field.name)}: ${E(field.before == null ? '∅' : field.before)} → ${E(field.after == null ? '∅' : field.after)}`).join('')}
` : ''; const text = change.kind === 'modified' ? `${E(change.identity || change.verb || '')}${fieldText}` : `${E(change.kind === 'added' ? change.after : change.before || '')}`; return `
  • ${T(`diffSmart.${change.kind || 'modified'}`)}
    ${text}
  • `; }; const body = sections.length ? sections.map(section => `
    ${E(section.name)}+${Number(section.added) || 0}−${Number(section.removed) || 0}~${Number(section.modified) || 0}${section.attention !== 'low' ? `${T(`diffSmart.${section.attention}Attention`)}` : ''}
    `).join('') : `
    ${T('diffSmart.noSemanticChanges')}
    `; return `

    ${T('diffSmart.title')}

    ${T('diffSmart.subtitle')}
    ${T('diffSmart.semanticChanges')} ${total}${T('diffSmart.sections')} ${Number(smart.sections_changed) || 0}${T('diffSmart.attention')} ${attention}
    ${I('shield')}${T('diffSmart.note')}
    ${body}
    `; }; M.diffResult = () => { const d = M.state.diff; if (!d) return C('diffConfigs.workspaceTitle', M.empty('ui.selectPair', '', 'diff')); const stats = d.stats || {}, search = M.state.diffSearch || ''; const baseRows = (d.lines || []).filter(line => !M.state.diffChangesOnly || line.type !== 'context'); const query = search.toLocaleLowerCase(); const allRows = query ? baseRows.filter(line => String(line.left_text || '').toLocaleLowerCase().includes(query) || String(line.right_text || '').toLocaleLowerCase().includes(query)) : baseRows; const pageSize = Math.max(80, Number(M.state.diffPageSize) || 200); const pages = Math.max(1, Math.ceil(allRows.length / pageSize)); M.state.diffPage = Math.min(pages, Math.max(1, Number(M.state.diffPage) || 1)); const start = (M.state.diffPage - 1) * pageSize; const rows = allRows.slice(start, start + pageSize); const pane = side => `
    ${E(d[`${side}_file_name`] || '')}
    ${rows.map(row => { const text = row[`${side}_text`] || '', n = row[`${side}_number`]; let type = ['context', 'added', 'removed', 'modified'].includes(row.type) ? row.type : 'context'; if (n == null) type = 'blank'; const match = search && text.toLocaleLowerCase().includes(search.toLocaleLowerCase()); return `
    ${n == null ? '' : Number(n)}${M.highlight(text, search) || ' '}
    `; }).join('')}
    `; const unified = () => `
    ${rows.map(row => { const lines = []; if (row.type === 'context') lines.push(['context', ' ', row.left_number, row.left_text]); else if (row.type === 'removed') lines.push(['removed', '−', row.left_number, row.left_text]); else if (row.type === 'added') lines.push(['added', '+', row.right_number, row.right_text]); else { if (row.left_number != null) lines.push(['removed', '−', row.left_number, row.left_text]); if (row.right_number != null) lines.push(['added', '+', row.right_number, row.right_text]); } return lines.map(([type, prefix, n, text]) => { const match = search && String(text || '').toLocaleLowerCase().includes(search.toLocaleLowerCase()); return `
    ${prefix}${n == null ? '' : Number(n)}${M.highlight(text, search) || ' '}
    `; }).join(''); }).join('')}
    `; const pager = pages > 1 ? `
    ${T('ui.page')} ${M.state.diffPage} / ${pages} · ${start + 1}–${Math.min(allRows.length, start + rows.length)} / ${allRows.length}
    ` : `${T('ui.lines')}: ${allRows.length}`; return `${M.smartDiff(d.smart)}
    ${[['added', 'ui.added'], ['removed', 'ui.removed'], ['modified', 'ui.modified'], ['context', 'ui.context']].map(([key, label]) => `${T(label)} ${Number(stats[key]) || 0}`).join('')}
    ${M.state.diffMode === 'split' ? `
    ${pane('left')}${pane('right')}
    ` : unified()}
    `; }; M.diffControls = () => { const files = (M.state.cache.diffFiles || []).filter(M.isTextExport), routers = (M.state.cache.routers || []).filter(M.supportsExport), s = M.state.diffSelection || {}; const options = [['', t('diffConfigs.noneSelected')], ...files.map(f => [f.id, `${f.router_name || '#' + f.router_id} · ${M.date(f.created_at)} · ${f.file_name}`])]; return `
    ${S('router_id', 'files.routerLabel', [['', t('files.allRouters')], ...routers.map(r => [r.id, r.name])], s.router_id || '')}${S('left', 'files.compareOlder', options, s.left || '', 'required')}${IB('swap-diff', 'ui.swap', 'swap')}${S('right', 'files.compareNewer', options, s.right || '', 'required')}
    `; }; M.views.diff = async () => { const revision = M.state.routeId; const s = M.state.diffSelection || {}; const query = M.backupQuery({ backup_type: 'export', router_id: s.router_id || '', sort_by: 'created_at', order: 'desc' }); let [files, routers] = await Promise.all([M.api(`/backups?${query}`), M.api('/routers')]); if (revision !== M.state.routeId) return ""; if (s.router_id && routers.some(router => String(router.id) === String(s.router_id) && !M.supportsExport(router))) { M.state.diffSelection = { router_id: '', left: '', right: '' }; M.state.diff = null; files = await M.api('/backups?backup_type=export&sort_by=created_at&order=desc'); if (revision !== M.state.routeId) return ''; history.replaceState({}, '', '/diff-configs'); } M.state.cache.diffFiles = files.filter(M.isTextExport); M.state.cache.routers = routers; const selection = M.state.diffSelection || s; const allowed = new Set(M.state.cache.diffFiles.map(file => Number(file.id))); M.state.diffSelection = { ...selection, left: allowed.has(Number(selection.left)) ? selection.left : '', right: allowed.has(Number(selection.right)) ? selection.right : '' }; if (M.state.diff && (!allowed.has(Number(M.state.diff.left_backup_id)) || !allowed.has(Number(M.state.diff.right_backup_id)))) M.state.diff = null; return `${M.heading('diffConfigs.title', 'diffConfigs.subtitle', B('latest-diff-pair', t('files.compareLatestPair'), 'clock', M.state.cache.diffFiles.length < 2 ? 'disabled' : '') + B('refresh', t('ui.refresh'), 'refresh'), 'diffConfigs.eyebrow')}
    ${M.diffControls()}
    ${M.diffResult()}
    ${I('files')}${T('files.binaryNoCompare')} · ${M.state.cache.diffFiles.length} ${T('files.exportPoolLabel')}
    `; }; M.schedulerTable = status => { const jobs = status.jobs || []; return `
    ${T('ui.timezone')}: ${E(status.timezone)}${M.badge(t(status.running ? 'settings.statusEnabled' : 'settings.statusDisabled'), status.running ? 'success' : 'muted')}
    ${jobs.map(job => `
    ${E(t(job.label || job.key))}${E(t(job.description || '', job.description_params || {}))}
    ${M.badge(t(!job.valid ? 'ui.invalid' : job.enabled ? 'settings.statusEnabled' : 'settings.statusDisabled'), !job.valid ? 'danger' : job.enabled ? 'success' : 'muted')}${E(job.cron || '—')}${job.error ? `${E(job.error)}` : ''}
    ${T('ui.nextRuns')}${(job.next_runs || []).length ? job.next_runs.map(date => ``).join('') : '—'}
    `).join('')}
    `; }; M.scheduleEditor = (prefix, title, expr, enabled = true) => { const parts = String(expr || '').trim().split(/\s+/); let mode = !expr || !enabled ? 'disabled' : 'custom', time = '02:00', weekday = 'mon'; const names = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; if (expr && enabled && parts.length === 5 && /^\d+$/.test(parts[0]) && /^\d+$/.test(parts[1]) && parts[2] === '*' && parts[3] === '*') { time = `${parts[1].padStart(2, '0')}:${parts[0].padStart(2, '0')}`; if (parts[4] === '*') mode = 'daily'; else if (/^[0-6]$/.test(parts[4]) || names.includes(parts[4].toLowerCase())) { mode = 'weekly'; weekday = /^\d$/.test(parts[4]) ? names[Number(parts[4])] : parts[4].toLowerCase(); } } return `

    ${T(title)}

    ${E(expr || t('settings.scheduleDisabledHint'))}

    ${S(`${prefix}_mode`, 'settings.scheduleMode', [['disabled', t('settings.scheduleDisabled')], ['daily', t('settings.scheduleDaily')], ['weekly', t('settings.scheduleWeekly')], ['custom', t('settings.scheduleCustom')]], mode)}
    ${F(`${prefix}_time`, 'settings.scheduleTime', time, { type: 'time' })}
    ${S(`${prefix}_weekday`, 'settings.scheduleWeekday', names.map((day, i) => [day, t('settings.weekday' + ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'][i])]), weekday)}
    ${F(`${prefix}_custom`, prefix === 'export' ? 'settings.exportCron' : prefix === 'binary' ? 'settings.binaryCron' : 'settings.retentionCron', expr || '', { attrs: 'spellcheck="false" autocomplete="off"', placeholder: '0 2 * * *' })}
    `; }; M.settingsTabKeys = ['general', 'automation', 'credentials', 'smtp', 'pushover', 'account']; M.settingsTabs = () => `
    ${[['general', 'settings.retentionTitle'], ['automation', 'ui.automation'], ['credentials', 'settings.sshDefaultsTitle'], ['smtp', 'settings.notificationsTitle'], ['pushover', 'Pushover'], ['account', 'ui.account']].map(([key, label]) => ``).join('')}
    `; M.secretField = (name, key, hasValue, type = 'password') => `
    ${F(name, key, '', { type, placeholder: t(hasValue ? 'ui.kept' : 'ui.notSet'), attrs: 'autocomplete="new-password" spellcheck="false"' })}
    `; M.settingsForm = settings => { const s = settings, hidden = key => M.state.settingsTab === key ? '' : ' hidden'; const general = `
    ${F('backup_retention_days', 'settings.backupRetentionDays', s.backup_retention_days, { type: 'number', required: true, attrs: 'min="0" max="36500"' })}${F('log_retention_days', 'settings.logRetentionDays', s.log_retention_days, { type: 'number', required: true, attrs: 'min="0" max="36500"' })}
    `; const schedules = `${M.scheduleEditor('export', 'settings.exportScheduleTitle', s.export_cron, s.enable_auto_export)}${M.scheduleEditor('binary', 'settings.binaryScheduleTitle', s.binary_cron)}${M.scheduleEditor('retention', 'settings.retentionTitle', s.retention_cron)}

    ${T('settings.connectionTestsTitle')}

    ${T('settings.connectionTestsHint')}

    ${F('connection_test_interval_minutes', 'settings.connectionTestIntervalMinutes', s.connection_test_interval_minutes, { type: 'number', attrs: 'min="0" max="1440"' })}
    `; const credentials = `

    ${T('settings.globalSshPrivateKey')}

    ${T('settings.sshRevealHint')}

    ${M.secretField('global_ssh_key', 'settings.globalSshPrivateKey', s.has_global_ssh_key, 'textarea')}
    ${B('reveal-ssh', t('settings.revealSshKey'), 'eye', s.has_global_ssh_key ? '' : 'disabled')}${s.has_global_ssh_key ? M.badge(t('settings.sshKeyStoredTag'), 'success') : ''}

    ${T('settings.switchosDefaultsTitle')}

    ${T('settings.switchosDefaultsHint')}

    ${F('default_switchos_username', 'settings.defaultSwitchosUsername', s.default_switchos_username || '', { attrs: 'autocomplete="off" maxlength="120"' })}${M.secretField('default_switchos_password', 'settings.defaultSwitchosPassword', s.has_default_switchos_password || s.has_default_switchos_credentials)}
    `; const smtp = `${M.toggle('smtp_notifications_enabled', 'settings.smtpEnabled', s.smtp_notifications_enabled, 'settings.smtpEnabledHint')}${M.toggle('notify_failures_only', 'settings.failuresOnly', s.notify_failures_only, 'settings.failuresOnlyHint')}
    ${F('smtp_host', 'settings.smtpHost', s.smtp_host || '', { attrs: 'maxlength="255"' })}${F('smtp_port', 'settings.smtpPort', s.smtp_port || 587, { type: 'number', attrs: 'min="1" max="65535"' })}${F('smtp_login', 'settings.smtpLogin', s.smtp_login || '', { attrs: 'autocomplete="off"' })}${M.secretField('smtp_password', 'settings.smtpPassword', s.has_smtp_password)}${F('recipient_email', 'settings.recipientEmail', s.recipient_email || '', { type: 'email', wide: true })}
    ${B('test-email', t('settings.testEmail'), 'mail')}${T('ui.saveFirst')}
    `; const pushover = `
    ${M.secretField('pushover_token', 'settings.pushoverToken', s.has_pushover_token)}${M.secretField('pushover_userkey', 'settings.pushoverUserKey', s.has_pushover_userkey)}
    ${B('test-pushover', t('settings.testPushover'), 'check')}${T('ui.saveFirst')}
    `; return `

    ${T('settings.retentionTitle')}

    ${T('settings.retentionSubtitle')}

    ${general}
    ${schedules}
    ${credentials}
    ${smtp}
    ${pushover}
    ${B('refresh', t('common.reset'), 'refresh')}
    `; }; M.preferencesForm = () => `

    ${T('settings.interfacePreferencesHint')}

    ${S('preferred_language', 'ui.language', [['pl', 'Polski'], ['en', 'English'], ['es', 'Español'], ['no', 'Norsk']], M.state.lang)}${S('preferred_font', 'settings.fontFamily', [['default', t('settings.fontDefault')], ['adwaita_mono', 'Adwaita Mono'], ['hack', 'Hack']], M.state.font)}
    `; M.views.settings = async () => { const revision = M.state.routeId; const [settings, status] = await Promise.all([M.api('/settings'), M.api('/settings/scheduler-status')]); if (revision !== M.state.routeId) return ""; M.state.cache.settings = settings; M.state.cache.scheduler = status; M.state.settingsTab ||= 'general'; return `${M.heading('settings.title', 'settings.subtitle', B('refresh', t('ui.refresh'), 'refresh'), 'settings.eyebrow')}
    ${M.settingsTabs()}
    ${M.settingsForm(settings)}
    ${C('auth.changePassword', M.passwordForm(), '', 'password-card')}

    ${T('ui.systemTasks')}

    ${IB('refresh-scheduler', 'ui.refresh', 'refresh')}
    ${M.schedulerTable(status)}
    ${C('ui.preferences', M.preferencesForm(), '', 'preference-card')}
    `; }; M.logsTable = () => { const term = (M.state.logSearch || '').toLocaleLowerCase(); const logs = (M.state.cache.logs || []).filter(log => String(log.message).toLocaleLowerCase().includes(term)); if (!logs.length) return M.empty(term ? 'ui.noMatches' : 'ui.noLogs', '', 'clock'); return `
    ${M.paged(logs).map(log => ``).join('')}
    ${T('logs.timestampColumn')}${T('logs.messageColumn')}
    ${E(M.date(log.timestamp))}${E(log.message)}
    ${M.pagination(logs.length)}`; }; M.views.logs = async () => { const revision = M.state.routeId; const [logs, settings] = await Promise.all([M.api('/logs'), M.api('/settings')]); if (revision !== M.state.routeId) return ""; M.state.cache.logs = logs; return `${M.heading('logs.title', 'logs.tableSubtitle', B('refresh', t('ui.refresh'), 'refresh'), 'logs.eyebrow')}
    ${T('files.searchLabel')}
    ${F('days', 'logs.deleteOlderThan', settings.log_retention_days ?? 7, { type: 'number', attrs: 'min="1" max="36500" required' })}
    ${M.logsTable()}
    `; }; M.passwordForm = () => `

    ${T('auth.changePasswordCardSubtitle')}

    ${F('current_password', 'auth.currentPassword', '', { type: 'password', required: true, wide: true, attrs: 'autocomplete="current-password"' })}${F('new_password', 'auth.newPassword', '', { type: 'password', required: true, attrs: 'autocomplete="new-password" minlength="4" maxlength="128"' })}${F('confirm_password', 'ui.confirmNewPassword', '', { type: 'password', required: true, attrs: 'autocomplete="new-password" minlength="4" maxlength="128"' })}
    ${I('check')}${T('auth.ruleLength')}${I('check')}${T('auth.ruleDigit')}${I('check')}${T('auth.ruleMatch')}
    `; M.languageSelect = () => ``; M.authView = register => { register = !!register && M.config.allowRegistration; return `

    MIKROTIK / BACKUP SYSTEM

    ${T('ui.protected')}

    ${T('ui.loginHint')}

    ${T(register ? 'auth.register' : 'auth.login')}

    ${T('auth.loginSubtitle')}

    ${F('username', 'auth.username', '', { required: true, attrs: 'autocomplete="username" minlength="3" maxlength="120"' })}${F('password', 'auth.password', '', { type: 'password', required: true, attrs: `autocomplete="${register ? 'new-password' : 'current-password'}" minlength="4" maxlength="128"` })}${register ? F('confirm_password', 'auth.confirmPassword', '', { type: 'password', required: true, attrs: 'autocomplete="new-password" minlength="4" maxlength="128"' }) : ''}
    ${register || M.config.allowRegistration ? `${T(register ? 'auth.backToLogin' : 'auth.register')}` : ''}
    `; }; })(window.MBS);