- The demo reset has set STORAGE_BACKEND=sqlite in .env.
- The change will take effect after a server restart.
- Until then the server is still running with the old backend.
+ ${t('maint_productionRestartDesc')}
@@ -4360,85 +4350,78 @@ async function renderAdminMaintenanceTab() { docker compose restart
- After restart, the system stores all data in data/isms.db (SQLite, WAL mode).
- JSON files are retained as a fallback backup.
+ ${t('maint_productionRestartAfter')}
Storage Backend
+${t('maint_backend')}
- JSON — for demo and development only.
- SQLite — required for production (ACID-compliant, WAL mode, no data loss on crash).
- The demo reset automatically sets STORAGE_BACKEND=sqlite in .env and
- requires a server restart afterwards.
+ ${t('maint_backendDesc')}
Backup & Export
+${t('maint_backup')}
- Exports all data (templates, SoA, risks, GDPR, users, settings) as a single JSON file. - Attachments (PDF/DOCX) are not included. + ${t('maint_backupDesc')}
Clean up orphaned attachments
+${t('maint_cleanupOrphans')}
- Removes files from the upload directory that are no longer associated with any template. + ${t('maint_cleanupOrphansDesc')}
AI Integration (Ollama)
+${t('maint_aiIntegration')}
- Controls semantic search and future AI features. Ollama must be installed and running locally. - If Ollama is unavailable, features can be disabled here — - the topbar search will then fall back to "unavailable". + ${t('maint_aiDesc')}
- Global switch — disables all AI features system-wide. + ${t('maint_aiGlobalSwitch')}
Greenbone Scan-Import
- Importiert einen Greenbone-Sicherheitsbericht (XML oder PDF) als Risiko-Entwürfe. - Alle importierten Risiken erhalten den Status needsReview = true und - müssen von einem Auditor oder CISO geprüft und freigegeben werden, - bevor sie im Risikomanagement aktiv sind. + ${t('scanImport_descPrefix')} + ${t('scanImport_descSuffix')}
Demo Reset
+${t('maint_demoResetTitle')}
- Exports all current demo data, then deletes all module data and users (except admin)
- and resets the admin account to adminpass without 2FA.
- The exported JSON file is automatically downloaded.
- After the reset, a notice for the administrator will appear on the login page.
+ ${t('maint_demoResetDesc')}
Import demo data
+${t('maint_demoImport')}
- Restores a previously exported demo dataset. All module data will be overwritten. - Users alice and bob will be restored with original passwords and without 2FA. - The admin account remains unchanged. + ${t('maint_demoImportDesc')}
Login Splash Screen
+${t('maint_splashScreen')}
- After login, show the ISMS Builder banner image for a few seconds before the dashboard opens. + ${t('maint_splashDesc')}
Loading trash…
' const res = await fetch('/trash', { headers: apiHeaders('admin') }) if (!res.ok) { - container.innerHTML = 'Error loading trash.
' + container.innerHTML = `${t('trash_loadError')}
` return } const items = await res.json() if (items.length === 0) { - container.innerHTML = 'The trash is empty.
' + container.innerHTML = `${t('trash_empty')}
` return } @@ -4802,14 +4778,14 @@ async function renderAdminTrashTab() { container.innerHTML = `${escHtml(label)} (${group.length})
| Title | Deleted By | Deleted At | Expires | Actions | |||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ${t('col_title')} | ${t('trash_deletedBy')} | ${t('trash_deletedAt')} | ${t('trash_expires')} | ${t('col_actions')} | ${escHtml(item.title || item.id)} | ${escHtml(item.deletedBy || '—')} | ${new Date(item.deletedAt).toLocaleDateString('en-GB')} | -${daysLeft} days | +${daysLeft} ${t('reports_days')} |
|
`
@@ -4836,7 +4812,7 @@ async function renderAdminTrashTab() {
}
async function restoreTrashItem(module, id, meta) {
- if (!confirm('Restore item?')) return
+ if (!confirm(t('trash_restoreItemConfirm'))) return
let url
if (module === 'template') url = `/template/${meta.type}/${id}/restore`
else if (module === 'risk') url = `/risks/${id}/restore`
@@ -4854,15 +4830,15 @@ async function restoreTrashItem(module, id, meta) {
else if (module === 'gdpr_toms') url = `/gdpr/toms/${id}/restore`
else if (module === 'public_incident') url = `/public/incident/${id}/restore`
else if (module === 'finding') url = `/findings/${id}/restore`
- else { alert('Unknown module'); return }
+ else { alert(t('trash_unknownModule')); return }
const res = await fetch(url, { method: 'POST', headers: apiHeaders('admin') })
- if (!res.ok) { alert('Restore failed'); return }
+ if (!res.ok) { alert(t('trash_restoreFailed')); return }
renderAdminTrashTab()
}
async function permanentDeleteTrashItem(module, id, meta) {
- if (!confirm('Permanently delete? This action CANNOT be undone!')) return
+ if (!confirm(t('trash_permanentDeleteConfirm'))) return
let url
if (module === 'template') url = `/template/${meta.type}/${id}/permanent`
else if (module === 'risk') url = `/risks/${id}/permanent`
@@ -4880,10 +4856,10 @@ async function permanentDeleteTrashItem(module, id, meta) {
else if (module === 'gdpr_toms') url = `/gdpr/toms/${id}/permanent`
else if (module === 'public_incident') url = `/public/incident/${id}/permanent`
else if (module === 'finding') url = `/findings/${id}/permanent`
- else { alert('Unknown module'); return }
+ else { alert(t('trash_unknownModule')); return }
const res = await fetch(url, { method: 'DELETE', headers: apiHeaders('admin') })
- if (!res.ok) { alert('Delete failed'); return }
+ if (!res.ok) { alert(t('trash_deleteFailed')); return }
renderAdminTrashTab()
}
@@ -4976,7 +4952,7 @@ const SOA_FW_META = [
async function renderAdminModulesTab() {
const container = document.getElementById('adminTabPanelModules')
if (!container) return
- container.innerHTML = '
| User | -Role | -Functions | -Domain | +${t('audit_resourceUser')} | +${t('admin_email')} | +${t('admin_role')} | +${t('admin_functions')} | +${t('admin_domain')} | ${fns} | ${escHtml(u.domain || '—')} |
- - ${isEdit ? 'Edit User' : 'New User'} + ${isEdit ? t('admin_editUser') : t('admin_newUser')}
-
+
-
+
-
+
-
+
-
+
${FUNCTIONS_LIST.map(f => `
`).join('')}
- Multiple functions possible — independent of RBAC rank +${t('admin_functionsHint')}
-
+
-
+
- `
@@ -5494,14 +5465,14 @@ function renderAdminEntitiesTab() {
async function adminLoadEntities() {
const tree = document.getElementById('adminEntityTree')
if (!tree) return
- tree.innerHTML = 'Corporate Structure – Entities+${t('admin_corporateEntities')}
-
Loading… ' + tree.innerHTML = `${t('loading')} ` try { const res = await fetch('/entities/tree', { headers: apiHeaders('reader') }) const roots = await res.json() tree.innerHTML = '' roots.forEach(e => tree.appendChild(renderEntityNode(e))) } catch (err) { - tree.innerHTML = `Error: ${err.message} ` + tree.innerHTML = `${t('error')}: ${err.message} ` } } @@ -5514,8 +5485,8 @@ function renderEntityNode(e) { ${e.name} ${e.shortCode || ''}
-
${e.children && e.children.length > 0 ? `${e.children.map(c => renderEntityNode(c).outerHTML).join('')} ` : ''}
@@ -5536,7 +5507,7 @@ function openEntityModal(entity) {
- ${isEdit ? 'Edit Entity' : 'New Entity'} + ${isEdit ? t('admin_editEntity') : t('admin_newEntity')}
-
+
-
+
-
+
-
Security Goals ISO 27001 Kap. 6.2+${t('goals_title')} ${t('goals_isoClause')}${goalCanEdit() ? `
- ${summary.total||0}Total
- ${summary.active||0}Active
- ${summary.achieved||0}Achieved
- ${summary.overdue||0}Overdue
+ ${summary.total||0}${t('common_total')}
+ ${summary.active||0}${t('goals_statusActive')}
+ ${summary.achieved||0}${t('goals_statusAchieved')}
+ ${summary.overdue||0}${t('reports_overdue')}
${summary.avgProgress||0}%
@@ -5702,7 +5673,7 @@ async function renderGoals() {
- ${list.length} Goal(s)
+ ${list.length} ${t('goals_countLabel')}
@@ -5736,7 +5707,7 @@ async function renderGoals() {
${g.owner ? ` ${escHtml(g.owner)}` : ''}
- ${g.targetDate ? ` ${new Date(g.targetDate).toLocaleDateString('en-GB')}${isOverdue?' (overdue)':''}` : ''}
+ ${g.targetDate ? ` ${new Date(g.targetDate).toLocaleDateString('en-GB')}${isOverdue ? ` (${t('reports_overdue').toLowerCase()})` : ''}` : ''}
${g.kpis?.length ? ` ${g.kpis.length} KPI(s)` : ''}
-
-
-
-
+
+
+
+
`
}
@@ -5802,58 +5773,58 @@ async function openGoalForm(id = null) {
container.innerHTML = `
-
${id ? 'Edit Security Goal' : 'New Security Goal'}+${id ? t('goals_edit') : t('goals_new')}
-
-
-
+
-
+
+
-
+
+
-
+
+
+
-
+
`
@@ -5867,7 +5838,7 @@ async function openGoalForm(id = null) {
async function saveGoal(id) {
const title = document.getElementById('goalTitle')?.value?.trim()
- if (!title) { alert('Name is required'); return }
+ if (!title) { alert(t('err_nameRequired')); return }
const kpis = [...document.querySelectorAll('.goal-kpi-row')].map(row => {
const inputs = row.querySelectorAll('input')
@@ -5900,14 +5871,14 @@ async function saveGoal(id) {
headers: { ...apiHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
- if (!res.ok) { const e = await res.json(); alert(e.error || 'Error'); return }
+ if (!res.ok) { const e = await res.json(); alert(e.error || t('error')); return }
renderGoals()
}
async function deleteGoal(id) {
- if (!confirm('Delete security goal?')) return
+ if (!confirm(t('goals_deleteConfirm'))) return
const res = await fetch(`/goals/${id}`, { method: 'DELETE', headers: apiHeaders() })
- if (!res.ok) { const e = await res.json(); alert(e.error || 'Error'); return }
+ if (!res.ok) { const e = await res.json(); alert(e.error || t('error')); return }
renderGoals()
}
@@ -6337,21 +6308,21 @@ async function tmplMgmtEdit(type, id) {
}
async function tmplMgmtSoftDelete(type, id, title) {
- if (!confirm(`Template "${title}" in den Papierkorb verschieben?`)) return
+ if (!confirm(t('tmpl_trashConfirm', { title }))) return
const res = await fetch(`/template/${encodeURIComponent(type)}/${encodeURIComponent(id)}`, { method: 'DELETE', headers: apiHeaders('contentowner') })
if (!res.ok) { const e = await res.json().catch(()=>({})); alert(e.error || 'Error'); return }
loadTmplManagement()
}
async function tmplMgmtPermDelete(type, id, title) {
- if (!confirm(`Template "${title}" endgültig löschen? Dies kann nicht rückgängig gemacht werden.`)) return
+ if (!confirm(t('tmpl_permanentDeleteConfirm', { title }))) return
const res = await fetch(`/template/${encodeURIComponent(type)}/${encodeURIComponent(id)}/permanent`, { method: 'DELETE', headers: apiHeaders('admin') })
if (!res.ok) { const e = await res.json().catch(()=>({})); alert(e.error || 'Error'); return }
loadTmplManagement()
}
async function tmplMgmtRestore(type, id, title) {
- if (!confirm(`Template "${title}" wiederherstellen?`)) return
+ if (!confirm(t('tmpl_restoreConfirm', { title }))) return
const res = await fetch(`/template/${encodeURIComponent(type)}/${encodeURIComponent(id)}/restore`, { method: 'POST', headers: apiHeaders('admin') })
if (!res.ok) { const e = await res.json().catch(()=>({})); alert(e.error || 'Error'); return }
loadTmplManagement()
@@ -7320,8 +7291,8 @@ async function renderGuidance() {
${c.label}
`).join('')}
-
+
-
+
+
-
- KPIs (Metrics)
- Define measurable metrics — progress is automatically calculated from the KPIs. + ${t('goals_kpisMetrics')}
+ ${t('goals_kpisDesc')}
- Metric / KPI
- Target
- Current
- Unit
+ ${t('goals_metricKpi')}
+ ${t('goals_target')}
+ ${t('goals_current')}
+ ${t('goals_unit')}
${kpisHtml}
+
-
@@ -7442,7 +7413,7 @@ function printGuidanceDoc(docId) {
function printGuidanceCategory() {
const docs = _guidanceDocs.filter(d => d.type === 'markdown' || d.type === 'html')
- if (docs.length === 0) return alert('Keine druckbaren Dokumente in dieser Kategorie.')
+ if (docs.length === 0) return alert(t('guidance_noPrintableDocs'))
_printGuidanceDocs(docs)
}
@@ -7463,7 +7434,7 @@ function _printGuidanceDocs(docs) {
}).join('')
const win = window.open('', '_blank')
- if (!win) return alert('Pop-up blockiert – bitte Pop-ups für diese Seite erlauben.')
+ if (!win) return alert(t('err_popupBlocked'))
win.document.write(`
|
- ${escHtml(r.title)}${r.needsReview ? ' ⚠ Review' : ''} | -${escHtml(cat?.label || r.category)} | +${escHtml(r.title)}${r.needsReview ? ` ⚠ Review` : ''} | +${escHtml(riskCatLabel(cat) || r.category)} | ${r.probability} × ${r.impact} = ${r.score} | -${escHtml(tr?.label || r.treatmentOption)} | +${escHtml(riskTreatmentLabel(tr) || r.treatmentOption)} | ${st?.label || r.status} | ${escHtml(r.owner || '—')} |
@@ -8242,12 +8216,12 @@ async function renderRiskHeatmap(el) {
${listRows}
- Low (1–4)
- Medium (5–9)
- High (10–14)
- Critical (15–25)
+ ${t('risk_levelLow')}
+ ${t('risk_levelMed')}
+ ${t('risk_levelHigh')}
+ ${t('risk_levelCrit')}
- Click a point to open risk details. ` +${t('risk_heatmapHint')} ` el.innerHTML = grid } @@ -8272,28 +8246,28 @@ async function renderRiskTreatments(el) { return new Date(a.dueDate) - new Date(b.dueDate) }) - const statusLabel = { open:'Offen', in_progress:'In Arbeit', completed:'Abgeschlossen' } + const statusLabel = { open:t('findings_statusOpen'), in_progress:t('findings_statusInProgress'), completed:t('risk_statusCompleted') } const today = new Date().toISOString().slice(0,10) const riskOpts = risks.map(r => ``).join('') el.innerHTML = `
-
- ${rows.length === 0 ? 'Alle Behandlungsmaßnahmen (${rows.length})+${t('risk_allTreatments')} (${rows.length})${canManageRisks() ? `No treatment measures recorded. ' : ` + ${rows.length === 0 ? `${t('risk_noMeasures')} ` : `
- Scan-Importe (${scanRisks.length} freigegeben)+${t('risk_scanImports')} (${scanRisks.length} ${t('risk_approved').toLowerCase()})
|
|---|