/* 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)}
`;
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 `${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 = `${days.map(day => `
${day.count}${E(day.label)} `).join('')}
`;
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('')}
`;
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}
`)}
`;
};
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 `| ${T('routers.name')} | ${T('ui.address')} | ${T('routers.model')} | ${T('ui.status')} | ${T('routers.ping')} | ${T('routers.lastTestAt')} | ${T('routers.backupPolicy')} | |
${M.paged(rows).map(r => `| ${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')} |
`).join('')}
${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')}`;
};
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 ``;
};
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', ``, '', '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}
`)}
${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')}${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`)}` : ''}
${(section.details || []).map(detail).join('')}${section.details_truncated ? `- ${T('diffSmart.moreChanges')}
` : ''}
`).join('') : `${T('diffSmart.noSemanticChanges')}
`;
return `${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`] || '')}
`;
const unified = () => ``;
const pager = pages > 1 ? `` : `${T('ui.lines')}: ${allRows.length}`;
return `${M.smartDiff(d.smart)}${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 ``;
};
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 ``;
};
M.preferencesForm = () => ``;
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')}${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 `| ${T('logs.timestampColumn')} | ${T('logs.messageColumn')} |
${M.paged(logs).map(log => `| ${E(M.date(log.timestamp))} | ${E(log.message)} |
`).join('')}
${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')}${M.logsTable()}
`;
};
M.passwordForm = () => ``;
M.languageSelect = () => ``;
M.authView = register => {
register = !!register && M.config.allowRegistration;
return `MIKROTIK / BACKUP SYSTEM
${T('ui.protected')}
${T('ui.loginHint')}
`;
};
})(window.MBS);