v0.0.2
This commit is contained in:
1 parent
c7d09077fc
commit
f813441bf0
45 files changed
+938
-334
No files matched your search
+43
-22
@@ -1,4 +1,3 @@
|
||||
/* 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;
|
||||
@@ -76,7 +75,7 @@
|
||||
return `${M.heading('routers.title', 'routers.subtitle', B('new-device', t('ui.newDevice'), 'plus', '', 'primary') + B('refresh', t('ui.refresh'), 'refresh'), 'routers.eyebrow')}<form id="device-filter" class="filter-bar"><div class="field search-field"><span>${T('files.searchLabel')}</span><input name="search" type="search" value="${E(f.search || '')}" placeholder="${T('ui.search')}" aria-label="${T('files.searchLabel')}"></div>${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')}</form><section class="card"><div id="device-table">${M.deviceTable()}</div></section>`;
|
||||
};
|
||||
M.deviceForm = router => {
|
||||
const editing = !!router, r = router || { name: '', host: '', port: 22, device_type: 'routeros', ssh_user: 'admin' };
|
||||
const editing = !!router, r = router || { name: '', host: '', port: 22, device_type: 'routeros', ssh_user: '' };
|
||||
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';
|
||||
@@ -90,7 +89,7 @@
|
||||
${F('port', 'routers.port', r.port, { type: 'number', required: true, attrs: 'min="1" max="65535"' })}
|
||||
<div class="wide" id="switchos-credentials-mode"${switchos ? '' : ' hidden'}>${S('use_global_switchos_credentials', 'ui.credentialSource', sources, source, switchos ? '' : 'disabled')}<p class="credential-hint" data-switchos-mode-hint${source === 'false' ? ' hidden' : ''}>${T(legacy ? 'ui.legacyCredentialsHint' : 'ui.globalCredentialsHint')}</p></div>
|
||||
<div class="form-grid wide" id="device-own-credentials"${global ? ' hidden' : ''}>
|
||||
${F('ssh_user', 'routers.sshUser', r.ssh_user || '', { attrs: 'maxlength="120" autocomplete="off"' + (global ? ' disabled' : '') })}
|
||||
${F('ssh_user', 'routers.sshUser', r.ssh_user || '', { attrs: 'maxlength="120" autocomplete="off"' + (global ? ' disabled' : ''), hint: switchos ? '' : t('routers.routerosDefaultFallback') })}
|
||||
<div class="secret-field">${F('ssh_password', 'routers.sshPassword', '', { type: 'password', attrs: 'maxlength="255" autocomplete="new-password"' + (global ? ' disabled' : ''), placeholder: editing ? t(passwordSet ? 'ui.kept' : 'ui.notSet') : '' })}${editing ? `<label class="checkbox-label"><input type="checkbox" name="clear_ssh_password"${global ? ' disabled' : ''}>${T('ui.clearSecret')}</label>` : ''}</div>
|
||||
</div>
|
||||
<div class="wide secret-field" id="device-key-field"${switchos ? ' hidden' : ''}>${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 ? `<label class="checkbox-label"><input type="checkbox" name="clear_ssh_key"${switchos ? ' disabled' : ''}>${T('ui.clearSecret')}</label>` : ''}</div>
|
||||
@@ -107,7 +106,7 @@
|
||||
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 `<div class="grid-equal detail-grid"><div class="stack">${C('routers.connectionSectionTitle', `<div class="card-body">${summary}</div>`)}${C('ui.backups', `<div class="card-body">${backupSummary}</div><div class="card-foot actions">${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')}</div>`)}</div><div class="stack">${C('routers.deviceStatusTitle', `<div class="card-body">${status}${r.last_connection_error ? `<div class="notice warning mt-18">${I('alert')}<span class="break">${E(r.last_connection_error)}</span></div>` : ''}${!r.last_connection_tested_at ? `<p class="section-hint mt-18">${T('routers.noConnection')}</p>` : ''}</div>`)}${C('routers.credentialsSectionTitle', `<div class="card-body">${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')]])}</div>`)}</div></div>`;
|
||||
return `<div class="grid-equal detail-grid"><div class="stack">${C('routers.connectionSectionTitle', `<div class="card-body">${summary}</div>`)}${C('ui.backups', `<div class="card-body">${backupSummary}</div><div class="card-foot actions">${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')}</div>`)}</div><div class="stack">${C('routers.deviceStatusTitle', `<div class="card-body">${status}${r.last_connection_error ? `<div class="notice warning mt-18">${I('alert')}<span class="break">${E(r.last_connection_error)}</span></div>` : ''}${!r.last_connection_tested_at ? `<p class="section-hint mt-18">${T('routers.noConnection')}</p>` : ''}</div>`)}${C('routers.credentialsSectionTitle', `<div class="card-body">${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.device_type === 'switchos' ? (r.uses_global_switchos_credentials ? 'common.enabled' : 'common.disabled') : (r.uses_default_routeros_credentials ? 'common.enabled' : 'common.disabled'))]])}</div>`)}</div></div>`;
|
||||
};
|
||||
M.views.device = async id => {
|
||||
const revision = M.state.routeId;
|
||||
@@ -124,7 +123,8 @@
|
||||
Object.entries(filters).forEach(([key, value]) => { if (value !== '' && value !== null && value !== undefined) params.set(key, String(value)); });
|
||||
return params.toString();
|
||||
};
|
||||
M.fileToolbar = () => `<div class="actions">${B('bulk-download', t('files.downloadZip'), 'download')}${B('compare-selected', t('ui.compare'), 'diff', 'hidden')}${B('bulk-delete', t('common.delete'), 'trash', '', 'danger')}<span class="select-summary">${T('ui.selection')}: <b id="selected-count">${M.state.selected.size}</b></span></div>`;
|
||||
M.fileToolbar = () => `<div class="actions">${B('upload-local-file', t('files.uploadToDevice'), 'upload', '', 'primary')}${B('bulk-download', t('files.downloadZip'), 'download')}${B('compare-selected', t('ui.compare'), 'diff', 'hidden')}${B('bulk-delete', t('common.delete'), 'trash', '', 'danger')}<span class="select-summary">${T('ui.selection')}: <b id="selected-count">${M.state.selected.size}</b></span></div>`;
|
||||
M.uploadFileForm = routers => `<form id="device-file-upload-form"><div class="form-grid">${S('router_id', 'files.uploadTarget', routers.map(r => [r.id, `${r.name} · ${r.host}`]), routers[0]?.id || '', 'required')}<label class="field wide" for="f-device_file"><span>${T('files.uploadFileLabel')}<span class="required"> *</span></span><input id="f-device_file" name="file" type="file" accept=".rsc,.backup" required><small>${T('files.uploadFileHint')}</small></label></div><div class="notice mt-16">${I('upload')}<span>${T('ui.uploadFile')}</span></div><div class="form-actions">${B('close-dialog', t('common.cancel'))}<button type="submit" class="btn primary">${I('upload')}${T('ui.upload')}</button></div></form>`;
|
||||
M.views.files = async () => {
|
||||
const revision = M.state.routeId;
|
||||
const f = M.state.filters;
|
||||
@@ -171,8 +171,8 @@
|
||||
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 start = M.state.diffShowAll ? 0 : (M.state.diffPage - 1) * pageSize;
|
||||
const rows = M.state.diffShowAll ? allRows : allRows.slice(start, start + pageSize);
|
||||
const pane = side => `<div class="diff-pane"><div class="diff-title" title="${E(d[`${side}_file_name`] || '')}">${E(d[`${side}_file_name`] || '')}</div><div class="diff-scroll" data-diff-scroll="${side}"><div class="diff-lines">${rows.map(row => {
|
||||
const text = row[`${side}_text`] || '', n = row[`${side}_number`];
|
||||
let type = ['context', 'added', 'removed', 'modified'].includes(row.type) ? row.type : 'context';
|
||||
@@ -188,30 +188,51 @@
|
||||
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 `<div class="diff-line ${type}${match ? ' match' : ''}"><span class="diff-prefix">${prefix}</span><span class="line-no">${n == null ? '' : Number(n)}</span><span class="line-code">${M.highlight(text, search) || ' '}</span></div>`; }).join('');
|
||||
}).join('')}</div></div>`;
|
||||
const pager = pages > 1 ? `<div class="diff-pager"><button type="button" class="btn small" data-action="diff-page-prev"${M.state.diffPage <= 1 ? ' disabled' : ''}>${T('ui.previous')}</button><span>${T('ui.page')} <strong>${M.state.diffPage}</strong> / ${pages} · ${start + 1}–${Math.min(allRows.length, start + rows.length)} / ${allRows.length}</span><button type="button" class="btn small" data-action="diff-page-next"${M.state.diffPage >= pages ? ' disabled' : ''}>${T('ui.next')}</button></div>` : `<span>${T('ui.lines')}: ${allRows.length}</span>`;
|
||||
return `${M.smartDiff(d.smart)}<section class="card raw-diff"><div class="diff-toolbar"><div class="tabs" role="tablist"><button type="button" class="tab ${M.state.diffMode === 'split' ? 'active' : ''}" data-diff-mode="split" role="tab" aria-selected="${M.state.diffMode === 'split'}">${T('ui.split')}</button><button type="button" class="tab ${M.state.diffMode === 'unified' ? 'active' : ''}" data-diff-mode="unified" role="tab" aria-selected="${M.state.diffMode === 'unified'}">${T('ui.unified')}</button></div><input type="search" id="diff-search" value="${E(search)}" placeholder="${T('ui.search')}" aria-label="${T('files.searchLabel')}"><label class="checkbox-label"><input type="checkbox" id="diff-changes-only"${M.state.diffChangesOnly ? ' checked' : ''}>${T('ui.changesOnly')}</label><div class="diff-stats">${[['added', 'ui.added'], ['removed', 'ui.removed'], ['modified', 'ui.modified'], ['context', 'ui.context']].map(([key, label]) => `<span class="${key}">${T(label)} <strong>${Number(stats[key]) || 0}</strong></span>`).join('')}</div></div>${M.state.diffMode === 'split' ? `<div class="diff-split">${pane('left')}${pane('right')}</div>` : unified()}<div class="diff-footer">${pager}<div class="actions">${B('download-diff', t('common.download') + ' .diff', 'download')}${B('download-html-diff', t('common.download') + ' HTML', 'files')}</div></div></section>`;
|
||||
const pager = M.state.diffShowAll ? `<span>${T('ui.lines')}: ${allRows.length}</span>` : pages > 1 ? `<div class="diff-pager"><button type="button" class="btn small" data-action="diff-page-prev"${M.state.diffPage <= 1 ? ' disabled' : ''}>${T('ui.previous')}</button><span>${T('ui.page')} <strong>${M.state.diffPage}</strong> / ${pages} · ${start + 1}–${Math.min(allRows.length, start + rows.length)} / ${allRows.length}</span><button type="button" class="btn small" data-action="diff-page-next"${M.state.diffPage >= pages ? ' disabled' : ''}>${T('ui.next')}</button></div>` : `<span>${T('ui.lines')}: ${allRows.length}</span>`;
|
||||
const fullToggle = pages > 1 || M.state.diffShowAll ? B('diff-toggle-all', t(M.state.diffShowAll ? 'ui.usePagination' : 'ui.loadFullDiff'), 'files') : '';
|
||||
return `${M.smartDiff(d.smart)}<section class="card raw-diff"><div class="diff-toolbar"><div class="tabs" role="tablist"><button type="button" class="tab ${M.state.diffMode === 'split' ? 'active' : ''}" data-diff-mode="split" role="tab" aria-selected="${M.state.diffMode === 'split'}">${T('ui.split')}</button><button type="button" class="tab ${M.state.diffMode === 'unified' ? 'active' : ''}" data-diff-mode="unified" role="tab" aria-selected="${M.state.diffMode === 'unified'}">${T('ui.unified')}</button></div><input type="search" id="diff-search" value="${E(search)}" placeholder="${T('ui.search')}" aria-label="${T('files.searchLabel')}"><label class="checkbox-label"><input type="checkbox" id="diff-changes-only"${M.state.diffChangesOnly ? ' checked' : ''}>${T('ui.changesOnly')}</label><div class="diff-stats">${[['added', 'ui.added'], ['removed', 'ui.removed'], ['modified', 'ui.modified'], ['context', 'ui.context']].map(([key, label]) => `<span class="${key}">${T(label)} <strong>${Number(stats[key]) || 0}</strong></span>`).join('')}</div></div>${M.state.diffMode === 'split' ? `<div class="diff-split">${pane('left')}${pane('right')}</div>` : unified()}<div class="diff-footer">${pager}<div class="actions">${fullToggle}${B('open-html-diff', t('ui.openStandaloneDiff'), 'globe')}${B('download-diff', t('common.download') + ' .diff', 'download')}${B('download-html-diff', t('common.download') + ' HTML', 'files')}</div></div></section>`;
|
||||
};
|
||||
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 `<form id="diff-form" class="diff-controls">${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')}<button class="btn primary" type="submit">${I('diff')}${T('ui.compare')}</button></form>`;
|
||||
const mode = s.mode === 'cross' ? 'cross' : 'same';
|
||||
const fileOptions = list => [['', t('diffConfigs.noneSelected')], ...list.map(f => [f.id, `${M.date(f.created_at)} · ${f.file_name}`])];
|
||||
const routerOptions = [['', t('diffConfigs.chooseDevice')], ...routers.map(r => [r.id, `${r.name} · ${r.host}`])];
|
||||
let selectors;
|
||||
if (mode === 'cross') {
|
||||
const leftFiles = s.left_router_id ? files.filter(f => String(f.router_id) === String(s.left_router_id)) : [];
|
||||
const rightFiles = s.right_router_id ? files.filter(f => String(f.router_id) === String(s.right_router_id)) : [];
|
||||
selectors = `${S('left_router_id', 'diffConfigs.leftDevice', routerOptions, s.left_router_id || '', 'required')}${S('left', 'files.compareOlder', fileOptions(leftFiles), s.left || '', 'required')}${IB('swap-diff', 'ui.swap', 'swap')}${S('right_router_id', 'diffConfigs.rightDevice', routerOptions, s.right_router_id || '', 'required')}${S('right', 'files.compareNewer', fileOptions(rightFiles), s.right || '', 'required')}`;
|
||||
} else {
|
||||
const scoped = s.router_id ? files.filter(f => String(f.router_id) === String(s.router_id)) : [];
|
||||
const options = fileOptions(scoped);
|
||||
selectors = `${S('router_id', 'files.routerLabel', routerOptions, s.router_id || '', 'required')}${S('left', 'files.compareOlder', options, s.left || '', 'required')}${IB('swap-diff', 'ui.swap', 'swap')}${S('right', 'files.compareNewer', options, s.right || '', 'required')}`;
|
||||
}
|
||||
return `<form id="diff-form" class="diff-controls ${mode === 'cross' ? 'cross-device' : ''}">${S('diff_scope', 'diffConfigs.compareMode', [['same', t('diffConfigs.sameDevice')], ['cross', t('diffConfigs.differentDevices')]], mode)}${selectors}<button class="btn primary" type="submit">${I('diff')}${T('ui.compare')}</button></form>`;
|
||||
};
|
||||
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')]);
|
||||
let [files, routers] = await Promise.all([M.api('/backups?backup_type=export&sort_by=created_at&order=desc'), 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 : '' };
|
||||
const routerIds = new Set(routers.filter(M.supportsExport).map(router => String(router.id)));
|
||||
const selection = { mode: s.mode === 'cross' ? 'cross' : 'same', router_id: s.router_id || '', left_router_id: s.left_router_id || '', right_router_id: s.right_router_id || '', left: s.left || '', right: s.right || '' };
|
||||
if (!routerIds.has(String(selection.router_id))) selection.router_id = '';
|
||||
if (!routerIds.has(String(selection.left_router_id))) selection.left_router_id = '';
|
||||
if (!routerIds.has(String(selection.right_router_id))) selection.right_router_id = '';
|
||||
if (!allowed.has(Number(selection.left))) selection.left = '';
|
||||
if (!allowed.has(Number(selection.right))) selection.right = '';
|
||||
if (selection.left && selection.right && !selection.router_id && !selection.left_router_id && !selection.right_router_id) {
|
||||
const leftFile = M.state.cache.diffFiles.find(file => String(file.id) === String(selection.left));
|
||||
const rightFile = M.state.cache.diffFiles.find(file => String(file.id) === String(selection.right));
|
||||
if (leftFile && rightFile && String(leftFile.router_id) === String(rightFile.router_id)) {
|
||||
selection.mode = 'same'; selection.router_id = leftFile.router_id;
|
||||
} else if (leftFile && rightFile) {
|
||||
selection.mode = 'cross'; selection.left_router_id = leftFile.router_id; selection.right_router_id = rightFile.router_id;
|
||||
}
|
||||
}
|
||||
M.state.diffSelection = selection;
|
||||
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')}<div id="diff-controls">${M.diffControls()}</div><div id="diff-result">${M.diffResult()}</div><div class="notice mt-16">${I('files')}<span>${T('files.binaryNoCompare')} · ${M.state.cache.diffFiles.length} ${T('files.exportPoolLabel')}</span></div>`;
|
||||
};
|
||||
@@ -237,7 +258,7 @@
|
||||
const s = settings, hidden = key => M.state.settingsTab === key ? '' : ' hidden';
|
||||
const general = `<div class="form-grid">${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"' })}</div>`;
|
||||
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)}<div class="automation-row"><div class="automation-caption"><h3>${T('settings.connectionTestsTitle')}</h3><p class="automation-note">${T('settings.connectionTestsHint')}</p></div><div class="schedule-fields">${F('connection_test_interval_minutes', 'settings.connectionTestIntervalMinutes', s.connection_test_interval_minutes, { type: 'number', attrs: 'min="0" max="1440"' })}</div></div>`;
|
||||
const credentials = `<h3 class="section-title">${T('settings.globalSshPrivateKey')}</h3><p class="section-hint">${T('settings.sshRevealHint')}</p>${M.secretField('global_ssh_key', 'settings.globalSshPrivateKey', s.has_global_ssh_key, 'textarea')}<div class="actions mt-15">${B('reveal-ssh', t('settings.revealSshKey'), 'eye', s.has_global_ssh_key ? '' : 'disabled')}${s.has_global_ssh_key ? M.badge(t('settings.sshKeyStoredTag'), 'success') : ''}</div><hr><h3 class="section-title">${T('settings.switchosDefaultsTitle')}</h3><p class="section-hint">${T('settings.switchosDefaultsHint')}</p><div class="form-grid">${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)}</div>`;
|
||||
const credentials = `<h3 class="section-title">${T('settings.globalSshPrivateKey')}</h3><p class="section-hint">${T('settings.sshRevealHint')}</p>${M.secretField('global_ssh_key', 'settings.globalSshPrivateKey', s.has_global_ssh_key, 'textarea')}<div class="actions mt-15">${B('reveal-ssh', t('settings.revealSshKey'), 'eye', s.has_global_ssh_key ? '' : 'disabled')}${s.has_global_ssh_key ? M.badge(t('settings.sshKeyStoredTag'), 'success') : ''}</div><hr><h3 class="section-title">${T('settings.routerosDefaultsTitle')}</h3><p class="section-hint">${T('settings.routerosDefaultsHint')}</p><div class="form-grid">${F('default_routeros_username', 'settings.defaultRouterosUsername', s.default_routeros_username || '', { attrs: 'autocomplete="off" maxlength="120"' })}${M.secretField('default_routeros_password', 'settings.defaultRouterosPassword', s.has_default_routeros_password || s.has_default_routeros_credentials)}</div><hr><h3 class="section-title">${T('settings.switchosDefaultsTitle')}</h3><p class="section-hint">${T('settings.switchosDefaultsHint')}</p><div class="form-grid">${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)}</div>`;
|
||||
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')}<div class="form-grid mt-22">${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 })}</div><div class="actions mt-22">${B('test-email', t('settings.testEmail'), 'mail')}<small>${T('ui.saveFirst')}</small></div>`;
|
||||
const pushover = `<div class="form-grid">${M.secretField('pushover_token', 'settings.pushoverToken', s.has_pushover_token)}${M.secretField('pushover_userkey', 'settings.pushoverUserKey', s.has_pushover_userkey)}</div><div class="actions mt-22">${B('test-pushover', t('settings.testPushover'), 'check')}<small>${T('ui.saveFirst')}</small></div>`;
|
||||
return `<form id="settings-form" class="settings-form"><div class="settings-fields card"><div class="settings-fields-body card-body"><section data-settings-pane="general"${hidden('general')}><h3 class="section-title">${T('settings.retentionTitle')}</h3><p class="section-hint">${T('settings.retentionSubtitle')}</p>${general}</section><section data-settings-pane="automation"${hidden('automation')}>${schedules}</section><section data-settings-pane="credentials"${hidden('credentials')}>${credentials}</section><section data-settings-pane="smtp"${hidden('smtp')}>${smtp}</section><section data-settings-pane="pushover"${hidden('pushover')}>${pushover}</section></div></div><div class="settings-save">${B('refresh', t('common.reset'), 'refresh')}<button type="submit" class="btn primary">${I('check')}${T('settings.save')}</button></div></form>`;
|
||||
|
||||
Reference in new issue
Block a user