You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

453 lines
15 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* Monitor-Check – Google Apps Script Backend
* ------------------------------------------
* Stellt eine JSON-API fuer das Web-Tool bereit.
* Deployment: Bereitstellen > Neue Bereitstellung > Web-App
* Ausfuehren als: Ich
* Zugriff: Jeder
*
* WICHTIG: API_TOKEN unten aendern und identisch im Frontend eintragen.
*/
var CONFIG = {
SHEET_NAME: 'Monitore',
LOG_SHEET_NAME: 'Protokoll',
API_TOKEN: 'BITTE-AENDERN-langes-zufaelliges-token',
ID_PREFIX: 'MON',
ID_PAD: 3
};
var HEADERS = [
'ID',
'Bezeichnung',
'Standort',
'Bild-URL',
'Audio',
'Bild',
'Sprache',
'Status',
'Notiz',
'Erfasst am',
'Geprueft am',
'Geprueft von'
];
var COL = {
ID: 0, BEZEICHNUNG: 1, STANDORT: 2, BILD_URL: 3,
AUDIO: 4, BILD: 5, SPRACHE: 6, STATUS: 7,
NOTIZ: 8, ERFASST: 9, GEPRUEFT: 10, PRUEFER: 11
};
/* ------------------------------------------------------------------ */
/* Status-Logik */
/* ------------------------------------------------------------------ */
var STATUS = {
OK: 'Funktionsfähig',
TEIL: 'Teilweise funktionsfähig',
FEHLER: 'Fehlerhaft',
AUS: 'Deaktiviert',
OFFEN: 'Nicht geprüft'
};
/**
* Regeln (audio/bild/sprache jeweils 'JA' | 'NEIN' | 'n.A.' | '')
* - Felder mit 'n.A.' werden aus der Bewertung ausgeschlossen.
* - Alle drei 'n.A.' -> Deaktiviert
* - Kein Feld gesetzt -> Nicht geprueft
* - Alle bewerteten Felder JA -> Funktionsfaehig
* - Alle bewerteten Felder NEIN -> Fehlerhaft
* - Gemischt -> Teilweise funktionsfaehig
*/
function berechneStatus(audio, bild, sprache) {
var werte = [norm_(audio), norm_(bild), norm_(sprache)];
var gesetzt = werte.filter(function (w) { return w !== ''; });
if (gesetzt.length === 0) return STATUS.OFFEN;
var na = werte.filter(function (w) { return w === 'n.A.'; }).length;
if (na === 3) return STATUS.AUS;
var bewertet = werte.filter(function (w) { return w === 'JA' || w === 'NEIN'; });
if (bewertet.length === 0) return STATUS.AUS;
var ja = bewertet.filter(function (w) { return w === 'JA'; }).length;
var nein = bewertet.filter(function (w) { return w === 'NEIN'; }).length;
if (nein === 0) return STATUS.OK;
if (ja === 0) return STATUS.FEHLER;
return STATUS.TEIL;
}
/**
* Vereinheitlicht Statustexte. Aeltere Zeilen wurden ohne Umlaute geschrieben
* ("Funktionsfaehig"); beide Schreibweisen ergeben denselben Status.
*/
function statusKanon_(v) {
var k = String(v == null ? '' : v).toLowerCase().trim()
.replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss');
if (k === 'funktionsfaehig') return STATUS.OK;
if (k === 'teilweise funktionsfaehig') return STATUS.TEIL;
if (k === 'fehlerhaft') return STATUS.FEHLER;
if (k === 'deaktiviert') return STATUS.AUS;
return STATUS.OFFEN;
}
function norm_(v) {
if (v === null || v === undefined) return '';
var s = String(v).trim().toUpperCase();
if (s === '') return '';
if (s === 'JA' || s === 'J' || s === 'YES' || s === 'TRUE') return 'JA';
if (s === 'NEIN' || s === 'N' || s === 'NO' || s === 'FALSE') return 'NEIN';
if (s === 'N.A.' || s === 'NA' || s === 'N/A' || s === 'N.A') return 'n.A.';
return '';
}
/* ------------------------------------------------------------------ */
/* HTTP-Einstieg */
/* ------------------------------------------------------------------ */
function doGet(e) {
return handle_(e, (e && e.parameter) ? e.parameter : {});
}
function doPost(e) {
var payload = {};
try {
if (e && e.postData && e.postData.contents) {
payload = JSON.parse(e.postData.contents);
}
} catch (err) {
return json_({ ok: false, error: 'Ungueltiges JSON' });
}
return handle_(e, payload);
}
function handle_(e, p) {
try {
if (String(p.token || '') !== CONFIG.API_TOKEN) {
return json_({ ok: false, error: 'Nicht autorisiert' });
}
switch (String(p.action || '')) {
case 'ping': return json_({ ok: true, version: 1, time: new Date().toISOString() });
case 'list': return json_({ ok: true, items: listeEintraege_() });
case 'create': return json_(erstelleEintrag_(p));
case 'status': return json_(setzeStatus_(p));
case 'update': return json_(aktualisiereEintrag_(p));
case 'archive': return json_(archiviereEintrag_(p));
case 'delete': return json_(loescheEintrag_(p));
default: return json_({ ok: false, error: 'Unbekannte Aktion' });
}
} catch (err) {
return json_({ ok: false, error: String(err && err.message ? err.message : err) });
}
}
function json_(obj) {
return ContentService
.createTextOutput(JSON.stringify(obj))
.setMimeType(ContentService.MimeType.JSON);
}
/* ------------------------------------------------------------------ */
/* Sheet-Zugriff */
/* ------------------------------------------------------------------ */
function sheet_() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName(CONFIG.SHEET_NAME);
if (!sh) {
sh = ss.insertSheet(CONFIG.SHEET_NAME);
}
if (sh.getLastRow() === 0 || String(sh.getRange(1, 1).getValue()).trim() === '') {
sh.getRange(1, 1, 1, HEADERS.length).setValues([HEADERS]);
sh.getRange(1, 1, 1, HEADERS.length).setFontWeight('bold');
sh.setFrozenRows(1);
}
return sh;
}
function daten_() {
var sh = sheet_();
var last = sh.getLastRow();
if (last < 2) return [];
return sh.getRange(2, 1, last - 1, HEADERS.length).getValues();
}
function zeileFuerId_(id) {
var rows = daten_();
for (var i = 0; i < rows.length; i++) {
if (String(rows[i][COL.ID]).trim() === String(id).trim()) {
return { row: i + 2, values: rows[i] };
}
}
return null;
}
function listeEintraege_() {
return daten_()
.filter(function (r) { return String(r[COL.ID]).trim() !== ''; })
.map(function (r) {
return {
id: String(r[COL.ID]),
bezeichnung: String(r[COL.BEZEICHNUNG] || ''),
standort: String(r[COL.STANDORT] || ''),
bildUrl: String(r[COL.BILD_URL] || ''),
audio: norm_(r[COL.AUDIO]),
bild: norm_(r[COL.BILD]),
sprache: norm_(r[COL.SPRACHE]),
status: statusKanon_(r[COL.STATUS]),
notiz: String(r[COL.NOTIZ] || ''),
erfasstAm: iso_(r[COL.ERFASST]),
geprueftAm: iso_(r[COL.GEPRUEFT]),
pruefer: String(r[COL.PRUEFER] || '')
};
});
}
function iso_(v) {
if (!v) return '';
if (v instanceof Date) return v.toISOString();
return String(v);
}
/* ------------------------------------------------------------------ */
/* Aktionen */
/* ------------------------------------------------------------------ */
function erstelleEintrag_(p) {
var bez = String(p.bezeichnung || '').trim();
if (!bez) return { ok: false, error: 'Bezeichnung fehlt' };
var lock = LockService.getScriptLock();
lock.waitLock(20000);
try {
var sh = sheet_();
var id = naechsteId_(sh);
var jetzt = new Date();
var audio = norm_(p.audio);
var bild = norm_(p.bild);
var sprache = norm_(p.sprache);
var status = berechneStatus(audio, bild, sprache);
var geprueft = status === STATUS.OFFEN ? '' : jetzt;
var zeile = [];
zeile[COL.ID] = id;
zeile[COL.BEZEICHNUNG] = bez;
zeile[COL.STANDORT] = String(p.standort || '').trim();
zeile[COL.BILD_URL] = String(p.bildUrl || '').trim();
zeile[COL.AUDIO] = audio;
zeile[COL.BILD] = bild;
zeile[COL.SPRACHE] = sprache;
zeile[COL.STATUS] = status;
zeile[COL.NOTIZ] = String(p.notiz || '').trim();
zeile[COL.ERFASST] = jetzt;
zeile[COL.GEPRUEFT] = geprueft;
zeile[COL.PRUEFER] = String(p.pruefer || '').trim();
sh.appendRow(zeile);
faerbeZeile_(sh, sh.getLastRow(), status);
protokolliere_(id, 'angelegt', status, p.pruefer);
return { ok: true, id: id, status: status };
} finally {
lock.releaseLock();
}
}
/**
* Naechste freie ID. Der hoechste je vergebene Zaehler wird zusaetzlich in den
* Skript-Eigenschaften gemerkt, damit IDs geloeschter Eintraege nie erneut
* vergeben werden.
*/
function naechsteId_(sh) {
var props = PropertiesService.getScriptProperties();
var max = parseInt(props.getProperty('lastId') || '0', 10);
if (isNaN(max)) max = 0;
var rows = daten_();
var re = new RegExp('^' + CONFIG.ID_PREFIX + '-(\\d+)$');
for (var i = 0; i < rows.length; i++) {
var m = re.exec(String(rows[i][COL.ID]).trim());
if (m) {
var n = parseInt(m[1], 10);
if (n > max) max = n;
}
}
var nummer = max + 1;
props.setProperty('lastId', String(nummer));
var next = String(nummer);
while (next.length < CONFIG.ID_PAD) next = '0' + next;
return CONFIG.ID_PREFIX + '-' + next;
}
function setzeStatus_(p) {
var treffer = zeileFuerId_(p.id);
if (!treffer) return { ok: false, error: 'ID nicht gefunden: ' + p.id };
var sh = sheet_();
var audio = norm_(p.audio);
var bild = norm_(p.bild);
var sprache = norm_(p.sprache);
var status = berechneStatus(audio, bild, sprache);
var jetzt = new Date();
sh.getRange(treffer.row, COL.AUDIO + 1, 1, 4)
.setValues([[audio, bild, sprache, status]]);
if (p.notiz !== undefined) {
sh.getRange(treffer.row, COL.NOTIZ + 1).setValue(String(p.notiz || ''));
}
sh.getRange(treffer.row, COL.GEPRUEFT + 1, 1, 2)
.setValues([[jetzt, String(p.pruefer || '')]]);
faerbeZeile_(sh, treffer.row, status);
protokolliere_(p.id, 'Status gesetzt', status, p.pruefer);
return { ok: true, id: String(p.id), status: status, geprueftAm: jetzt.toISOString() };
}
function aktualisiereEintrag_(p) {
var treffer = zeileFuerId_(p.id);
if (!treffer) return { ok: false, error: 'ID nicht gefunden: ' + p.id };
var sh = sheet_();
if (p.bezeichnung !== undefined) sh.getRange(treffer.row, COL.BEZEICHNUNG + 1).setValue(String(p.bezeichnung));
if (p.standort !== undefined) sh.getRange(treffer.row, COL.STANDORT + 1).setValue(String(p.standort));
if (p.bildUrl !== undefined) sh.getRange(treffer.row, COL.BILD_URL + 1).setValue(String(p.bildUrl));
if (p.notiz !== undefined) sh.getRange(treffer.row, COL.NOTIZ + 1).setValue(String(p.notiz));
protokolliere_(p.id, 'bearbeitet', '', p.pruefer);
return { ok: true, id: String(p.id) };
}
function archiviereEintrag_(p) {
var treffer = zeileFuerId_(p.id);
if (!treffer) return { ok: false, error: 'ID nicht gefunden: ' + p.id };
var sh = sheet_();
sh.getRange(treffer.row, COL.AUDIO + 1, 1, 4).setValues([['n.A.', 'n.A.', 'n.A.', STATUS.AUS]]);
faerbeZeile_(sh, treffer.row, STATUS.AUS);
protokolliere_(p.id, 'deaktiviert', STATUS.AUS, p.pruefer);
return { ok: true, id: String(p.id), status: STATUS.AUS };
}
function loescheEintrag_(p) {
var treffer = zeileFuerId_(p.id);
if (!treffer) return { ok: false, error: 'ID nicht gefunden: ' + p.id };
var lock = LockService.getScriptLock();
lock.waitLock(20000);
try {
// Zeile koennte sich zwischenzeitlich verschoben haben – erneut suchen.
treffer = zeileFuerId_(p.id);
if (!treffer) return { ok: false, error: 'ID nicht gefunden: ' + p.id };
var bez = String(treffer.values[COL.BEZEICHNUNG] || '');
var bild = String(treffer.values[COL.BILD_URL] || '');
sheet_().deleteRow(treffer.row);
protokolliere_(p.id, 'geloescht (' + bez + ')', bild, p.pruefer);
return { ok: true, id: String(p.id) };
} finally {
lock.releaseLock();
}
}
/* ------------------------------------------------------------------ */
/* Darstellung + Protokoll */
/* ------------------------------------------------------------------ */
function faerbeZeile_(sh, row, status) {
var farben = {};
farben[STATUS.OK] = '#e6f4ea';
farben[STATUS.TEIL] = '#fef7e0';
farben[STATUS.FEHLER] = '#fce8e6';
farben[STATUS.AUS] = '#f1f3f4';
farben[STATUS.OFFEN] = '#ffffff';
sh.getRange(row, 1, 1, HEADERS.length).setBackground(farben[status] || '#ffffff');
}
function protokolliere_(id, aktion, status, pruefer) {
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var log = ss.getSheetByName(CONFIG.LOG_SHEET_NAME);
if (!log) {
log = ss.insertSheet(CONFIG.LOG_SHEET_NAME);
log.appendRow(['Zeitpunkt', 'ID', 'Aktion', 'Status', 'Person']);
log.getRange(1, 1, 1, 5).setFontWeight('bold');
log.setFrozenRows(1);
}
log.appendRow([new Date(), String(id || ''), String(aktion || ''), String(status || ''), String(pruefer || '')]);
} catch (err) {
// Protokoll ist optional – Fehler hier duerfen die Aktion nicht abbrechen.
}
}
/* ------------------------------------------------------------------ */
/* Einmalige Einrichtung / Menue */
/* ------------------------------------------------------------------ */
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('Monitor-Check')
.addItem('Tabelle einrichten', 'einrichten')
.addItem('Alle Status neu berechnen', 'statusNeuBerechnen')
.addToUi();
}
function einrichten() {
var sh = sheet_();
sh.setColumnWidth(COL.ID + 1, 90);
sh.setColumnWidth(COL.BEZEICHNUNG + 1, 200);
sh.setColumnWidth(COL.STANDORT + 1, 160);
sh.setColumnWidth(COL.BILD_URL + 1, 260);
sh.setColumnWidth(COL.STATUS + 1, 190);
sh.setColumnWidth(COL.NOTIZ + 1, 260);
var jn = SpreadsheetApp.newDataValidation()
.requireValueInList(['JA', 'NEIN', 'n.A.'], true)
.setAllowInvalid(false)
.build();
sh.getRange(2, COL.AUDIO + 1, 1000, 3).setDataValidation(jn);
protokolliere_('', 'Tabelle eingerichtet', '', Session.getActiveUser().getEmail());
SpreadsheetApp.getUi().alert('Tabelle "' + CONFIG.SHEET_NAME + '" ist eingerichtet.');
}
function statusNeuBerechnen() {
var sh = sheet_();
var rows = daten_();
for (var i = 0; i < rows.length; i++) {
if (String(rows[i][COL.ID]).trim() === '') continue;
var s = berechneStatus(rows[i][COL.AUDIO], rows[i][COL.BILD], rows[i][COL.SPRACHE]);
sh.getRange(i + 2, COL.STATUS + 1).setValue(s);
faerbeZeile_(sh, i + 2, s);
}
}
/* ------------------------------------------------------------------ */
/* Selbsttest (im Editor ausfuehrbar) */
/* ------------------------------------------------------------------ */
function testStatusLogik() {
var faelle = [
[['JA', 'JA', 'JA'], STATUS.OK],
[['NEIN', 'NEIN', 'NEIN'], STATUS.FEHLER],
[['JA', 'NEIN', 'JA'], STATUS.TEIL],
[['JA', 'NEIN', 'NEIN'], STATUS.TEIL],
[['n.A.', 'n.A.', 'n.A.'], STATUS.AUS],
[['n.A.', 'JA', 'JA'], STATUS.OK],
[['n.A.', 'NEIN', 'NEIN'], STATUS.FEHLER],
[['n.A.', 'JA', 'NEIN'], STATUS.TEIL],
[['', '', ''], STATUS.OFFEN]
];
var fehler = [];
faelle.forEach(function (f) {
var ist = berechneStatus(f[0][0], f[0][1], f[0][2]);
if (ist !== f[1]) fehler.push(f[0].join('/') + ' -> ' + ist + ' (erwartet ' + f[1] + ')');
});
if (fehler.length) throw new Error('Fehlgeschlagen:\n' + fehler.join('\n'));
Logger.log('Alle ' + faelle.length + ' Testfaelle bestanden.');
}

Powered by TurnKey Linux.