// ════════════════════════════════════════════════════════════════════════ // modernize.today — Assessment Automation // Google Apps Script | Version 1.0 | März 2026 // // SETUP (einmalig): // 1. Diesen Code in Erweiterungen → Apps Script einfügen // 2. Funktion triggerEinrichten() einmalig ausführen // 3. Berechtigungen erteilen // 4. Mit testMitBeispieldaten() testen // // ABHÄNGIGKEITEN: // - Google Sheet mit Tabs: Formularantworten 1 | Scoring | Portfolio | Konfiguration // - Google Docs Template (URL in Konfiguration!B23) // - Google Drive Ordner (ID in Konfiguration!B24) // ════════════════════════════════════════════════════════════════════════ 'use strict'; // ── Dimensionsnamen (global) ───────────────────────────────────────────── const DIM_NAMEN = [ 'Kontext & Vision', 'Business Value & Stakeholder', 'Architektur & Technologie-Stack', 'Team, Organisation & Kompetenzen', 'Betrieb, Deployments & Infrastruktur', 'Daten & Integrationen', 'Sicherheit & Compliance', 'Risiken & Herausforderungen', 'Kosten & Lizenzmodelle', ]; // ════════════════════════════════════════════════════════════════════════ // KONFIGURATION AUS SHEET LESEN // ════════════════════════════════════════════════════════════════════════ function getConfig() { const cfg = SpreadsheetApp.getActiveSpreadsheet() .getSheetByName('Konfiguration'); if (!cfg) throw new Error('Tab "Konfiguration" nicht gefunden.'); return { gewichtungen: [ Number(cfg.getRange('B5').getValue()), // Dim 1 — Kontext & Vision Number(cfg.getRange('B6').getValue()), // Dim 2 — Business Value Number(cfg.getRange('B7').getValue()), // Dim 3 — Architektur Number(cfg.getRange('B8').getValue()), // Dim 4 — Team Number(cfg.getRange('B9').getValue()), // Dim 5 — Betrieb Number(cfg.getRange('B10').getValue()), // Dim 6 — Daten Number(cfg.getRange('B11').getValue()), // Dim 7 — Sicherheit Number(cfg.getRange('B12').getValue()), // Dim 8 — Risiken Number(cfg.getRange('B13').getValue()), // Dim 9 — Kosten ], schwelle_gruen: Number(cfg.getRange('B18').getValue()), schwelle_gelb: Number(cfg.getRange('B19').getValue()), template_url: String(cfg.getRange('B23').getValue()).trim(), ordner_id: String(cfg.getRange('B24').getValue()).trim(), cc_email: String(cfg.getRange('B25').getValue()).trim(), absender_name: String(cfg.getRange('B26').getValue()).trim() || 'modernize.today', }; } // ════════════════════════════════════════════════════════════════════════ // TRIGGER-EINSTIEGSPUNKT (wird automatisch bei Formular-Abgabe aufgerufen) // ════════════════════════════════════════════════════════════════════════ function onFormSubmit(e) { try { const config = getConfig(); const antworten = parseFormAntworten(e.namedValues); const scores = berechneScores(antworten, config); const report = erstelleReport(antworten, scores, config); schreibeInScoringSheet(antworten, scores, report.pdfUrl); sendeReportEmail(antworten, scores, report, config); Logger.log('✅ Assessment erfolgreich: ' + antworten.assessment_id + ' | Score: ' + scores.gesamt); } catch (err) { Logger.log('❌ FEHLER: ' + err.message + '\n' + err.stack); MailApp.sendEmail( Session.getEffectiveUser().getEmail(), '[modernize.today] Fehler bei Assessment-Verarbeitung', 'Fehler:\n' + err.message + '\n\nStack:\n' + err.stack ); } } // ════════════════════════════════════════════════════════════════════════ // FORMULAR-ANTWORTEN PARSEN // ════════════════════════════════════════════════════════════════════════ function parseFormAntworten(nv) { const get = (key) => { const v = nv[key]; return v ? String(v[0]).trim() : ''; }; const num = (key) => { const v = parseFloat(get(key)); return isNaN(v) ? 0 : v; }; const ts = new Date(); const id = 'APP-' + Utilities.formatDate(ts, 'Europe/Berlin', 'yyyyMMdd-HHmm'); return { timestamp: ts, assessment_id: id, app_name: get('Name der Applikation') || '(unbekannt)', beschreibung: get('Beschreibung (1–2 Sätze)'), projekt: get('Projektname / Mandant'), business_owner: get('Business Owner / Auftraggeber'), assessor: get('Assessor (Name)'), assessor_email: get('E-Mail-Adresse'), datum: get('Assessment-Datum') || Utilities.formatDate(ts, 'Europe/Berlin', 'dd.MM.yyyy'), hosting: get('Hosting-Umgebung'), architektur: get('Architekturtyp'), schnittstellen: get('Anzahl relevanter Schnittstellen'), security_review: get('Letzter Security Review'), kosten_rahmen: get('Kostenrahmen (jährlich, geschätzt)'), strategie: get('Empfohlene Modernisierungsstrategie'), zeitrahmen: get('Zeitrahmen für empfohlene Maßnahmen'), prioritaet: get('Priorität im Portfolio'), freitext_gesamt: get('Freitext: Weitere Beobachtungen und Empfehlungen'), freitext_kernfunktion: get('Freitext: Was ist die Kernfunktion dieser Applikation?'), freitext_geschaeftsprozesse: get('Freitext: Welche Geschäftsprozesse hängen kritisch von dieser Applikation ab?'), freitext_risiken: get('Freitext: Welche bekannten Risiken existieren?'), // Dimension 1 — Kontext & Vision d1q1: num('Es gibt eine klar definierte strategische Zielsetzung für diese Applikation.'), d1q2: num('Die Rolle dieser Applikation im IT-Portfolio ist bekannt und dokumentiert.'), d1q3: num('Die Zukunftsfähigkeit dieser Applikation ist unternehmensseitig bewertet worden.'), // Dimension 2 — Business Value d2q1: num('Der Business Impact bei einem vollständigen Ausfall wäre erheblich.'), d2q2: num('Die Applikation wird aktiv und regelmäßig genutzt.'), d2q3: num('Es gibt identifizierte und engagierte Business Owner.'), d2q4: num('Der Nutzen der Applikation ist klar messbar.'), // Dimension 3 — Architektur d3q1: num('Die Architektur ist modern und wartungsfreundlich.'), d3q2: num('Der Technologie-Stack ist aktuell und wird aktiv vom Hersteller supported.'), d3q3: num('Der technische Schuldenstand ist bekannt und beherrschbar.'), d3q4: num('Die Applikation ist erweiterbar und anpassungsfähig.'), // Dimension 4 — Team d4q1: num('Es gibt ausreichend internes Know-how für Betrieb und Weiterentwicklung.'), d4q2: num('Das Wissen über die Applikation ist auf mehrere Personen verteilt (kein Bus-Faktor).'), d4q3: num('Es gibt aktuelle und nutzbare Dokumentation.'), d4q4: num('Das Team kann eigenständig Changes und Bugfixes durchführen.'), // Dimension 5 — Betrieb d5q1: num('Der Betrieb ist stabil (wenig ungeplante Ausfälle, geringe Incident-Rate).'), d5q2: num('Deployments sind automatisiert oder zumindest standardisiert.'), d5q3: num('Die Infrastruktur ist skalierbar und auf aktuellem Stand.'), d5q4: num('Monitoring und Alerting sind vorhanden und funktionsfähig.'), // Dimension 6 — Daten d6q1: num('Schnittstellen und Integrationen sind dokumentiert.'), d6q2: num('Die Datenqualität ist bekannt und ausreichend.'), d6q3: num('Datenabhängigkeiten zu anderen Systemen sind beherrschbar.'), // Dimension 7 — Sicherheit d7q1: num('Aktuelle Sicherheitsstandards (OWASP, Patch-Level) werden eingehalten.'), d7q2: num('Compliance-Anforderungen (DSGVO, ISO 27001 o.ä.) sind erfüllt.'), d7q3: num('Es gibt einen definierten Prozess für Security Reviews und Updates.'), // Dimension 8 — Risiken d8q1: num('Das Ausfallrisiko ist gering und beherrschbar.'), d8q2: num('Es gibt keine kritischen Single Points of Failure.'), d8q3: num('Abhängigkeiten zu End-of-Life-Komponenten sind minimal.'), // Dimension 9 — Kosten d9q1: num('Die Gesamtbetriebskosten (TCO) sind bekannt und dokumentiert.'), d9q2: num('Das Kosten-Nutzen-Verhältnis ist angemessen.'), d9q3: num('Lizenzen und Verträge sind langfristig gesichert (kein baldiges End-of-Life).'), }; } // ════════════════════════════════════════════════════════════════════════ // SCORES BERECHNEN // ════════════════════════════════════════════════════════════════════════ function berechneScores(a, config) { const avg = (...vals) => vals.reduce((s, v) => s + v, 0) / vals.length; const rnd = (v) => Math.round(v * 10) / 10; // Durchschnitt je Dimension const dim = [ avg(a.d1q1, a.d1q2, a.d1q3), avg(a.d2q1, a.d2q2, a.d2q3, a.d2q4), avg(a.d3q1, a.d3q2, a.d3q3, a.d3q4), avg(a.d4q1, a.d4q2, a.d4q3, a.d4q4), avg(a.d5q1, a.d5q2, a.d5q3, a.d5q4), avg(a.d6q1, a.d6q2, a.d6q3), avg(a.d7q1, a.d7q2, a.d7q3), avg(a.d8q1, a.d8q2, a.d8q3), avg(a.d9q1, a.d9q2, a.d9q3), ].map(rnd); // Gewichteter Gesamtscore (0–100) const gew = config.gewichtungen; const gewSum = gew.reduce((s, g) => s + g, 0); const gesamt = rnd( dim.reduce((s, d, i) => s + d * gew[i], 0) / gewSum * 20 ); // Ampel je Dimension const ampelD = (s) => s >= 4.0 ? '🟢' : s >= 2.5 ? '🟡' : '🔴'; // Risikoklasse Gesamt const risikoklasse = gesamt >= config.schwelle_gruen ? '🟢 Modernisierungsbereit' : gesamt >= config.schwelle_gelb ? '🟡 Handlungsbedarf' : '🔴 Kritisch'; // Automatische Handlungsempfehlung const empfehlung = gesamt >= 80 ? 'Retain — Applikation ist stabil und zukunftsfähig. Gezielte Optimierungen genügen.' : gesamt >= 65 ? 'Optimize — Schwächen in einzelnen Dimensionen beheben, keine Grundsatzentscheidung nötig.' : gesamt >= 50 ? 'Refactor — Struktureller Umbau erforderlich. Architektur und technische Schulden adressieren.' : gesamt >= 35 ? 'Replatform — Plattformwechsel evaluieren. Bestehende Logik auf neuer Basis betreiben.' : 'Replace / Retire — Ablösung durch Standardsoftware oder Neuentwicklung prüfen.'; return { dim, ampel: dim.map(ampelD), gesamt, risikoklasse, empfehlung, }; } // ════════════════════════════════════════════════════════════════════════ // REPORT ERSTELLEN (Docs klonen + Platzhalter ersetzen + PDF) // ════════════════════════════════════════════════════════════════════════ function extrahiereFileId(url) { const match = url.match(/[-\w]{25,}/); if (!match) throw new Error('Ungültige Template-URL: ' + url); return match[0]; } function erstelleReport(antworten, scores, config) { const templateId = extrahiereFileId(config.template_url); const ordner = DriveApp.getFolderById(config.ordner_id); const dateiName = [ 'Assessment', antworten.app_name.replace(/[^a-zA-Z0-9äöüÄÖÜß\-_]/g, '_'), Utilities.formatDate(new Date(), 'Europe/Berlin', 'yyyy-MM-dd'), ].join('_'); // Template klonen const klon = DriveApp.getFileById(templateId).makeCopy(dateiName, ordner); const doc = DocumentApp.openById(klon.getId()); const body = doc.getBody(); // Hilfsfunktion: Text ersetzen (mit Fallback "—") const ersetze = (placeholder, wert) => { body.replaceText(placeholder, wert && String(wert).trim() ? String(wert) : '—'); }; // ── Basisdaten ────────────────────────────────────────────────────── ersetze('\\{\\{APP_NAME\\}\\}', antworten.app_name); ersetze('\\{\\{BESCHREIBUNG\\}\\}', antworten.beschreibung); ersetze('\\{\\{PROJEKT\\}\\}', antworten.projekt); ersetze('\\{\\{ASSESSOR\\}\\}', antworten.assessor); ersetze('\\{\\{BUSINESS_OWNER\\}\\}', antworten.business_owner); ersetze('\\{\\{DATUM\\}\\}', antworten.datum); ersetze('\\{\\{ASSESSMENT_ID\\}\\}', antworten.assessment_id); // ── Scores ────────────────────────────────────────────────────────── ersetze('\\{\\{GESAMT_SCORE\\}\\}', scores.gesamt.toString()); ersetze('\\{\\{RISIKOKLASSE\\}\\}', scores.risikoklasse); ersetze('\\{\\{EMPFEHLUNG_AUTO\\}\\}', scores.empfehlung); // ── Strategie / Planung ───────────────────────────────────────────── ersetze('\\{\\{STRATEGIE\\}\\}', antworten.strategie); ersetze('\\{\\{ZEITRAHMEN\\}\\}', antworten.zeitrahmen); ersetze('\\{\\{PRIORITAET\\}\\}', antworten.prioritaet); ersetze('\\{\\{NAECHSTER_SCHRITT\\}\\}', antworten.strategie + ' (' + antworten.zeitrahmen + ')'); // ── Freitexte ─────────────────────────────────────────────────────── ersetze('\\{\\{FREITEXT_KERNFUNKTION\\}\\}', antworten.freitext_kernfunktion); ersetze('\\{\\{FREITEXT_GESCHAEFTSPROZESSE\\}\\}', antworten.freitext_geschaeftsprozesse); ersetze('\\{\\{FREITEXT_RISIKEN\\}\\}', antworten.freitext_risiken); ersetze('\\{\\{WEITERE_BEOBACHTUNGEN\\}\\}', antworten.freitext_gesamt); // ── Technisches ───────────────────────────────────────────────────── ersetze('\\{\\{HOSTING\\}\\}', antworten.hosting); ersetze('\\{\\{ARCHITEKTUR\\}\\}', antworten.architektur); ersetze('\\{\\{SCHNITTSTELLEN\\}\\}', antworten.schnittstellen); ersetze('\\{\\{SECURITY_REVIEW\\}\\}', antworten.security_review); ersetze('\\{\\{KOSTEN_RAHMEN\\}\\}', antworten.kosten_rahmen); // ── Dimension-Scores (1–9) ────────────────────────────────────────── for (let i = 0; i < 9; i++) { const n = i + 1; ersetze(`\\{\\{DIM${n}_SCORE\\}\\}`, scores.dim[i].toString()); ersetze(`\\{\\{DIM${n}_AMPEL\\}\\}`, scores.ampel[i]); ersetze(`\\{\\{DIM${n}_NAME\\}\\}`, DIM_NAMEN[i]); ersetze(`\\{\\{DIM${n}_GEWICHT\\}\\}`, config.gewichtungen[i].toString()); // Bewertungstext je Score const bewText = bewertungstext(scores.dim[i]); ersetze(`\\{\\{DIM${n}_BEWERTUNG\\}\\}`, bewText); } doc.saveAndClose(); // Als PDF speichern const pdfBlob = DriveApp.getFileById(klon.getId()) .getAs(MimeType.PDF); pdfBlob.setName(dateiName + '.pdf'); const pdfFile = ordner.createFile(pdfBlob); return { docUrl: klon.getUrl(), pdfUrl: pdfFile.getUrl(), pdfBlob: pdfBlob, dateiName: dateiName, }; } // Bewertungstext basierend auf Dimension-Score function bewertungstext(score) { if (score >= 4.5) return 'Sehr gut — keine wesentlichen Maßnahmen erforderlich.'; if (score >= 3.5) return 'Gut — einzelne Verbesserungspotenziale identifiziert.'; if (score >= 2.5) return 'Befriedigend — Handlungsbedarf in dieser Dimension vorhanden.'; if (score >= 1.5) return 'Schwach — erhebliche Defizite, kurzfristige Maßnahmen empfohlen.'; return 'Kritisch — sofortige Maßnahmen erforderlich.'; } // ════════════════════════════════════════════════════════════════════════ // SCORING-SHEET BESCHREIBEN // ════════════════════════════════════════════════════════════════════════ function schreibeInScoringSheet(a, scores, reportUrl) { const sheet = SpreadsheetApp.getActiveSpreadsheet() .getSheetByName('Scoring'); if (!sheet) throw new Error('Tab "Scoring" nicht gefunden.'); const zeile = sheet.getLastRow() + 1; sheet.getRange(zeile, 1, 1, 21).setValues([[ a.timestamp, // A a.assessment_id, // B a.app_name, // C a.projekt, // D a.assessor_email, // E a.datum, // F scores.dim[0], // G Dim1 scores.dim[1], // H Dim2 scores.dim[2], // I Dim3 scores.dim[3], // J Dim4 scores.dim[4], // K Dim5 scores.dim[5], // L Dim6 scores.dim[6], // M Dim7 scores.dim[7], // N Dim8 scores.dim[8], // O Dim9 a.strategie, // P scores.gesamt, // Q scores.risikoklasse,// R scores.empfehlung, // S reportUrl, // T 'Report erstellt', // U ]]); } // ════════════════════════════════════════════════════════════════════════ // E-MAIL VERSENDEN // ════════════════════════════════════════════════════════════════════════ function sendeReportEmail(a, scores, report, config) { if (!a.assessor_email) { Logger.log('Kein Empfänger — E-Mail übersprungen.'); return; } const betreff = '[modernize.today] Assessment Report: ' + a.app_name + ' (' + scores.gesamt + '/100)'; // Score-Zeilen für die HTML-Tabelle const dimZeilen = DIM_NAMEN.map((name, i) => ` ${name} ${scores.dim[i]} / 5 ${scores.ampel[i]} `).join(''); const ampelFarbe = scores.gesamt >= config.schwelle_gruen ? '#10B981' : scores.gesamt >= config.schwelle_gelb ? '#F59E0B' : '#EF4444'; const htmlBody = `
modernize.today Application Assessment Report

${a.app_name}

Projekt: ${a.projekt || '—'}  ·  Assessor: ${a.assessor || '—'}  ·  ${a.datum}

${scores.gesamt}
Modernization Score (von 100)
${scores.risikoklasse}
${dimZeilen}
Dimension Score Status
Handlungsempfehlung ${scores.empfehlung}
Strategie ${a.strategie || '—'}
Zeitrahmen ${a.zeitrahmen || '—'}
Priorität ${a.prioritaet || '—'}
📄 PDF-Report öffnen    📝 Google Docs öffnen

Assessment-ID: ${a.assessment_id}  ·  modernize.today — Klarheit vor der Modernisierung

`; GmailApp.sendEmail(a.assessor_email, betreff, '', { htmlBody: htmlBody, name: config.absender_name, cc: config.cc_email || '', attachments: [report.pdfBlob], }); Logger.log('E-Mail gesendet an: ' + a.assessor_email); } // ════════════════════════════════════════════════════════════════════════ // AUTO-SETUP (einmalig ausführen — trägt Template-URL + Ordner-ID ein) // ════════════════════════════════════════════════════════════════════════ function setupAutoConfig() { const PARENT_FOLDER_ID = '1ag3QTet_3VHkC0Bw78Of7TEWMZVxbK_3'; // Reports-Unterordner suchen const parentFolder = DriveApp.getFolderById(PARENT_FOLDER_ID); const subfolders = parentFolder.getFolders(); let reportsFolderId = ''; while (subfolders.hasNext()) { const f = subfolders.next(); if (f.getName() === 'Reports') { reportsFolderId = f.getId(); break; } } if (!reportsFolderId) throw new Error('Unterordner "Reports" nicht gefunden.'); // Template-Datei (modernize_report_template) suchen const files = parentFolder.getFiles(); let templateUrl = ''; while (files.hasNext()) { const f = files.next(); if (f.getName() === 'modernize_report_template') { templateUrl = f.getUrl(); break; } } if (!templateUrl) throw new Error('Datei "modernize_report_template" nicht gefunden.'); // In Konfiguration-Tab eintragen const cfg = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Konfiguration'); cfg.getRange('B23').setValue(templateUrl); cfg.getRange('B24').setValue(reportsFolderId); SpreadsheetApp.getUi().alert( '✅ Konfiguration automatisch eingetragen!\n\n' + 'Template-URL: ' + templateUrl + '\n\n' + 'Reports-Ordner-ID: ' + reportsFolderId + '\n\n' + 'Bitte noch prüfen:\n' + ' • B25 = CC E-Mail (optional)\n' + ' • B26 = Absender-Name\n\n' + 'Dann triggerEinrichten() ausführen.' ); } // ════════════════════════════════════════════════════════════════════════ // TRIGGER EINRICHTEN (einmalig ausführen) // ════════════════════════════════════════════════════════════════════════ function triggerEinrichten() { // Bestehende Trigger entfernen ScriptApp.getProjectTriggers().forEach(t => ScriptApp.deleteTrigger(t)); // Neuen Form-Submit-Trigger anlegen ScriptApp.newTrigger('onFormSubmit') .forSpreadsheet(SpreadsheetApp.getActiveSpreadsheet()) .onFormSubmit() .create(); SpreadsheetApp.getUi().alert( '✅ Trigger eingerichtet!\n\n' + 'onFormSubmit wird automatisch bei jeder Formular-Abgabe ausgelöst.\n' + 'Jetzt mit testMitBeispieldaten() testen.' ); } // ════════════════════════════════════════════════════════════════════════ // TESTFUNKTION (ohne echtes Formular ausführbar) // ════════════════════════════════════════════════════════════════════════ function testMitBeispieldaten() { const t = (v) => [String(v)]; onFormSubmit({ namedValues: { 'Name der Applikation': t('Kundenportal v2'), 'Beschreibung (1–2 Sätze)': t('Das Kundenportal ermöglicht Self-Service und Auftragsverfolgung für Endkunden.'), 'Projektname / Mandant': t('ACME GmbH'), 'Business Owner / Auftraggeber': t('Max Mustermann'), 'Assessor (Name)': t('Anna Beispiel'), 'E-Mail-Adresse': t(Session.getEffectiveUser().getEmail()), 'Assessment-Datum': t('17.03.2026'), 'Hosting-Umgebung': t('Public Cloud'), 'Architekturtyp': t('Monolith'), 'Anzahl relevanter Schnittstellen': t('3–10'), 'Letzter Security Review': t('6–18 Monate'), 'Kostenrahmen (jährlich, geschätzt)': t('50–200k€'), 'Empfohlene Modernisierungsstrategie': t('Refactor'), 'Zeitrahmen für empfohlene Maßnahmen': t('Kurzfristig (3–12 Monate)'), 'Priorität im Portfolio': t('Hoch'), 'Freitext: Weitere Beobachtungen und Empfehlungen': t('Technische Schulden im Frontend sind erheblich. jQuery veraltet.'), 'Freitext: Was ist die Kernfunktion dieser Applikation?': t('Kunden-Self-Service und Auftragsverwaltung.'), 'Freitext: Welche Geschäftsprozesse hängen kritisch von dieser Applikation ab?': t('Auftragserfassung, Reklamationsmanagement, Statusverfolgung.'), 'Freitext: Welche bekannten Risiken existieren?': t('Kein Monitoring vorhanden. Veraltetes jQuery-Frontend. Einzelner Entwickler mit vollständigem Know-how.'), // Dim 1 'Es gibt eine klar definierte strategische Zielsetzung für diese Applikation.': t('3'), 'Die Rolle dieser Applikation im IT-Portfolio ist bekannt und dokumentiert.': t('4'), 'Die Zukunftsfähigkeit dieser Applikation ist unternehmensseitig bewertet worden.': t('2'), // Dim 2 'Der Business Impact bei einem vollständigen Ausfall wäre erheblich.': t('5'), 'Die Applikation wird aktiv und regelmäßig genutzt.': t('5'), 'Es gibt identifizierte und engagierte Business Owner.': t('4'), 'Der Nutzen der Applikation ist klar messbar.': t('3'), // Dim 3 'Die Architektur ist modern und wartungsfreundlich.': t('2'), 'Der Technologie-Stack ist aktuell und wird aktiv vom Hersteller supported.': t('2'), 'Der technische Schuldenstand ist bekannt und beherrschbar.': t('2'), 'Die Applikation ist erweiterbar und anpassungsfähig.': t('2'), // Dim 4 'Es gibt ausreichend internes Know-how für Betrieb und Weiterentwicklung.': t('3'), 'Das Wissen über die Applikation ist auf mehrere Personen verteilt (kein Bus-Faktor).': t('2'), 'Es gibt aktuelle und nutzbare Dokumentation.': t('2'), 'Das Team kann eigenständig Changes und Bugfixes durchführen.': t('4'), // Dim 5 'Der Betrieb ist stabil (wenig ungeplante Ausfälle, geringe Incident-Rate).': t('3'), 'Deployments sind automatisiert oder zumindest standardisiert.': t('2'), 'Die Infrastruktur ist skalierbar und auf aktuellem Stand.': t('3'), 'Monitoring und Alerting sind vorhanden und funktionsfähig.': t('2'), // Dim 6 'Schnittstellen und Integrationen sind dokumentiert.': t('3'), 'Die Datenqualität ist bekannt und ausreichend.': t('4'), 'Datenabhängigkeiten zu anderen Systemen sind beherrschbar.': t('3'), // Dim 7 'Aktuelle Sicherheitsstandards (OWASP, Patch-Level) werden eingehalten.': t('2'), 'Compliance-Anforderungen (DSGVO, ISO 27001 o.ä.) sind erfüllt.': t('3'), 'Es gibt einen definierten Prozess für Security Reviews und Updates.': t('2'), // Dim 8 'Das Ausfallrisiko ist gering und beherrschbar.': t('3'), 'Es gibt keine kritischen Single Points of Failure.': t('2'), 'Abhängigkeiten zu End-of-Life-Komponenten sind minimal.': t('2'), // Dim 9 'Die Gesamtbetriebskosten (TCO) sind bekannt und dokumentiert.': t('3'), 'Das Kosten-Nutzen-Verhältnis ist angemessen.': t('4'), 'Lizenzen und Verträge sind langfristig gesichert (kein baldiges End-of-Life).': t('4'), } }); }