initial files
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
plugins/
|
||||||
|
secure/
|
||||||
|
configuration.json
|
||||||
|
stylesheet.json
|
||||||
|
.npmrc
|
||||||
|
radix_os_*.png
|
||||||
|
radix_os_icon.ico
|
||||||
4
license_internal.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
© 2025 Grünflächenamt | Manuel Sowada
|
||||||
|
|
||||||
|
Diese Software ist ausschließlich für den internen dienstlichen Gebrauch durch Mitarbeiter des Grünflächenamtes vorgesehen.
|
||||||
|
Weitergabe, Veröffentlichung oder private Nutzung ist ohne ausdrückliche Genehmigung untersagt.
|
||||||
4937
package-lock.json
generated
Normal file
50
package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "radixos",
|
||||||
|
"version": "0.9",
|
||||||
|
"description": "Webbasiertes Betriebssystem für das Grünflächenamt Frankfurt am Main",
|
||||||
|
"main": "server.js",
|
||||||
|
"_moduleAliases": {
|
||||||
|
"@root": "./",
|
||||||
|
"@plugins": "./plugins",
|
||||||
|
"@public": "./public",
|
||||||
|
"@source": "./src",
|
||||||
|
"@services": "./src/services",
|
||||||
|
"@models": "./src/models",
|
||||||
|
"@routes": "./src/routes",
|
||||||
|
"@sockets": "./src/sockets"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"author": "Grünflächenamt - Manuel Sowada",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"licensefile": "license_internal.txt",
|
||||||
|
"dependencies": {
|
||||||
|
"activedirectory2": "^2.2.0",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"child_process": "^1.0.2",
|
||||||
|
"chokidar": "^4.0.3",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"express-handlebars": "^8.0.3",
|
||||||
|
"fs": "^0.0.1-security",
|
||||||
|
"fs-extra": "^11.3.2",
|
||||||
|
"https": "^1.0.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"ldapjs": "^3.0.7",
|
||||||
|
"module-alias": "^2.2.3",
|
||||||
|
"multer": "^2.0.2",
|
||||||
|
"net": "^1.0.2",
|
||||||
|
"npm": "^11.6.4",
|
||||||
|
"oracledb": "^6.10.0",
|
||||||
|
"os": "^0.1.2",
|
||||||
|
"p-limit": "^7.2.0",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"sequelize": "^6.37.7",
|
||||||
|
"serve-favicon": "^2.5.1",
|
||||||
|
"socket.io": "^4.8.1",
|
||||||
|
"tedious": "^18.6.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
72
public/helpers/comparisonHelpers.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
const Handlebars = require('handlebars');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
toJSON: function(object) {
|
||||||
|
if(typeof object === 'object') {
|
||||||
|
return JSON.stringify(object);
|
||||||
|
} else {
|
||||||
|
throw 'no object type';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isObject: function(value, options) {
|
||||||
|
return typeof value === 'object' && value !== null && !Array.isArray(value) ? options.fn(this) : options.inverse(this);
|
||||||
|
},
|
||||||
|
isArray: function(value, options) {
|
||||||
|
return Array.isArray(value) ? options.fn(this) : options.inverse(this);
|
||||||
|
},
|
||||||
|
isRGB: function(value) {
|
||||||
|
return Array.isArray(value) && (value.length === 3 || value.length === 4);
|
||||||
|
},
|
||||||
|
rgbString: function(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.length === 3
|
||||||
|
? `rgb(${value.join(',')})`
|
||||||
|
: value.length === 4
|
||||||
|
? `rgba(${value.join(',')})`
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
toArray: function(value) {
|
||||||
|
if (Array.isArray(value)) return value;
|
||||||
|
if (typeof value === "string") return [value];
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
replaceAll: function(string, pattern, replacement) {
|
||||||
|
return new Handlebars.SafeString(string.replaceAll(pattern, replacement) || '');
|
||||||
|
},
|
||||||
|
equaler: function(v1, operator, v2, options) {
|
||||||
|
switch (operator) {
|
||||||
|
case '==':
|
||||||
|
return (v1 == v2) ? options.fn(this) : options.inverse(this);
|
||||||
|
case '===':
|
||||||
|
return (v1 === v2) ? options.fn(this) : options.inverse(this);
|
||||||
|
case '!=':
|
||||||
|
return (v1 != v2) ? options.fn(this) : options.inverse(this);
|
||||||
|
case '!==':
|
||||||
|
return (v1 !== v2) ? options.fn(this) : options.inverse(this);
|
||||||
|
case '<':
|
||||||
|
return (v1 < v2) ? options.fn(this) : options.inverse(this);
|
||||||
|
case '<=':
|
||||||
|
return (v1 <= v2) ? options.fn(this) : options.inverse(this);
|
||||||
|
case '>':
|
||||||
|
return (v1 > v2) ? options.fn(this) : options.inverse(this);
|
||||||
|
case '>=':
|
||||||
|
return (v1 >= v2) ? options.fn(this) : options.inverse(this);
|
||||||
|
case '&&':
|
||||||
|
return (v1 && v2) ? options.fn(this) : options.inverse(this);
|
||||||
|
case '||':
|
||||||
|
return (v1 || v2) ? options.fn(this) : options.inverse(this);
|
||||||
|
case 'typeof':
|
||||||
|
return (typeof v1 === v2) ? options.fn(this) : options.inverse(this);
|
||||||
|
case 'like':
|
||||||
|
return (v1.indexOf(v2)) > -1 ? options.fn(this) : options.inverse(this);
|
||||||
|
case 'includes':
|
||||||
|
return (v1.includes(v2)) <= 0 ? options.inverse(this) : options.fn(this);
|
||||||
|
case '%':
|
||||||
|
return (v1 % v2) == 0 ? options.fn(this) : options.inverse(this);
|
||||||
|
default:
|
||||||
|
return options.inverse(this);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
6
public/helpers/dateHelpers.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// const moment = require('moment'); // npm install moment
|
||||||
|
// moment.locale('de');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
dateFormat: (date, format) => dateFormat(date, format)
|
||||||
|
};
|
||||||
24
public/helpers/fileHelper.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
module.exports = {
|
||||||
|
// objectTree: function(context, options) {
|
||||||
|
// let ret = '';
|
||||||
|
|
||||||
|
// if (context === null || context === undefined) return ret;
|
||||||
|
|
||||||
|
// if (Array.isArray(context)) {
|
||||||
|
// context.forEach((item, index) => {
|
||||||
|
// ret += options.fn({ key: index, value: item });
|
||||||
|
// });
|
||||||
|
// } else if (typeof context === 'object') {
|
||||||
|
// for (const key in context) {
|
||||||
|
// if (context.hasOwnProperty(key)) {
|
||||||
|
// ret += options.fn({ key, value: context[key] });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // primitive Werte
|
||||||
|
// ret += options.fn({ key: null, value: context });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return ret;
|
||||||
|
// }
|
||||||
|
}
|
||||||
145
public/helpers/iterationHelpers.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
module.exports = {
|
||||||
|
objectTree: function(obj) {
|
||||||
|
function traverse(o) {
|
||||||
|
if (o === null || typeof o !== 'object') return o;
|
||||||
|
|
||||||
|
if (Array.isArray(o)) {
|
||||||
|
// RGB oder RGBA-Erkennung: Array aus 3 oder 4 Zahlen
|
||||||
|
if ((o.length === 3 || o.length === 4) && o.every(v => typeof v === 'number')) {
|
||||||
|
// const rgbString = o.length === 3
|
||||||
|
// ? `rgb(${o.join(',')})`
|
||||||
|
// : `rgba(${o.join(',')})`;
|
||||||
|
const rgbString = o.join(',');
|
||||||
|
return { key: null, value: rgbString, children: null, isRGB: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// normales Array
|
||||||
|
return o.map((v, i) => ({
|
||||||
|
key: i,
|
||||||
|
value: typeof v !== 'object' ? v : null,
|
||||||
|
children: typeof v === 'object' ? traverse(v) : null
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
for (const key in o) {
|
||||||
|
if (o.hasOwnProperty(key)) {
|
||||||
|
const val = o[key];
|
||||||
|
const children = (val !== null && typeof val === 'object') ? traverse(val) : null;
|
||||||
|
result.push({
|
||||||
|
key,
|
||||||
|
value: children ? null : val,
|
||||||
|
children
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return traverse(obj);
|
||||||
|
},
|
||||||
|
jsonEntriesRecursive: function(obj, options) {
|
||||||
|
const entries = [];
|
||||||
|
|
||||||
|
function traverse(prefix, data) {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
data.forEach((item, index) => {
|
||||||
|
const arrayKey = `${prefix}[${index}]`;
|
||||||
|
if (typeof item === "object" && item !== null) {
|
||||||
|
traverse(arrayKey, item); // Rekursion für Array-Objekte
|
||||||
|
} else {
|
||||||
|
entries.push({ key: arrayKey, value: item });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (typeof data === "object" && data !== null) {
|
||||||
|
for (const key in data) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(data, key)) continue;
|
||||||
|
|
||||||
|
const value = data[key];
|
||||||
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||||
|
|
||||||
|
if (typeof value === "object" && value !== null) {
|
||||||
|
traverse(fullKey, value); // Rekursion für verschachtelte Objekte
|
||||||
|
} else {
|
||||||
|
entries.push({ key: fullKey, value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Primitive Werte am obersten Level
|
||||||
|
entries.push({ key: prefix, value: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log({ key: prefix, value: data })
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse("", obj);
|
||||||
|
|
||||||
|
// Handlebars-Block Helper
|
||||||
|
if (options.fn) {
|
||||||
|
return options.fn(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
},
|
||||||
|
groupBy: function(items, keys, options) {
|
||||||
|
// keys kann String oder Array sein
|
||||||
|
if (!Array.isArray(keys)) keys = [keys];
|
||||||
|
if (!keys.length) return '';
|
||||||
|
|
||||||
|
const key = keys[0]; // aktueller Gruppierungsschlüssel
|
||||||
|
const remainingKeys = keys.slice(1); // restliche Schlüssel
|
||||||
|
|
||||||
|
// Gruppieren nach aktuellem Schlüssel
|
||||||
|
const groups = {};
|
||||||
|
items.forEach(item => {
|
||||||
|
let groupKeys = item[key];
|
||||||
|
if (!Array.isArray(groupKeys)) groupKeys = [groupKeys];
|
||||||
|
|
||||||
|
groupKeys.forEach(groupKey => {
|
||||||
|
if (!groups[groupKey]) groups[groupKey] = [];
|
||||||
|
groups[groupKey].push(item);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ergebnis zusammensetzen
|
||||||
|
let result = '';
|
||||||
|
for (const group in groups) {
|
||||||
|
const groupItems = groups[group];
|
||||||
|
|
||||||
|
if (remainingKeys.length > 0) {
|
||||||
|
// Rekursive Verschachtelung
|
||||||
|
// Wir rufen die gleiche Helper-Funktion intern auf
|
||||||
|
const nestedResult = Handlebars.helpers.groupBy(groupItems, remainingKeys, options);
|
||||||
|
result += options.fn({ key: group, items: nestedResult });
|
||||||
|
} else {
|
||||||
|
// Basisfall: keine weiteren Keys, items direkt weitergeben
|
||||||
|
result += options.fn({ key: group, items: groupItems });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
parseArray: function(str){
|
||||||
|
try {
|
||||||
|
return JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ifSingle: function(array, options) {
|
||||||
|
if (Array.isArray(array) && array.length === 1) {
|
||||||
|
return options.fn(array[0]); // Item direkt übergeben
|
||||||
|
}
|
||||||
|
return options.inverse(this); // else-Block
|
||||||
|
},
|
||||||
|
findValue: function(array, searchKey, searchValue, returnKey) {
|
||||||
|
if (!Array.isArray(array)) return "";
|
||||||
|
for (let i = 0; i < array.length; i++) {
|
||||||
|
const item = array[i];
|
||||||
|
|
||||||
|
if (item && item[searchKey] === searchValue) {
|
||||||
|
return item[returnKey] ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
BIN
public/images/app.png
Normal file
|
After Width: | Height: | Size: 729 B |
BIN
public/images/arrow_left.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/images/arrow_right.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/images/brush.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/images/cursor_dark.png
Normal file
|
After Width: | Height: | Size: 957 B |
BIN
public/images/cursor_light.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
public/images/cursor_modern.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/images/cursor_pointer_dark.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/images/cursor_pointer_light.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/images/cursor_pointer_modern.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/images/eventlog.ico
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/images/folder.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
public/images/help.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/images/notifybubble.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/images/plugins.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/images/serverinfo.png
Normal file
|
After Width: | Height: | Size: 914 B |
BIN
public/images/tutorial.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
361
public/javascript/JSON.js
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
/*
|
||||||
|
createJsonTree({
|
||||||
|
container: domElement,
|
||||||
|
data: jsonData,
|
||||||
|
onChange: (data) => { }, // optional: callback on change
|
||||||
|
onSave: (data) => { } // optional: if set, a save button will be added
|
||||||
|
});
|
||||||
|
*/function createJsonTree({
|
||||||
|
container,
|
||||||
|
data,
|
||||||
|
onChange = () => {},
|
||||||
|
onSave = null
|
||||||
|
}) {
|
||||||
|
container.innerHTML = "";
|
||||||
|
container.classList.add("json-tree-root");
|
||||||
|
|
||||||
|
const history = [];
|
||||||
|
const redoStack = [];
|
||||||
|
|
||||||
|
let lastSnapshot = clone(data);
|
||||||
|
|
||||||
|
function clone(obj) {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
function diff(oldObj, newObj) {
|
||||||
|
const changes = {};
|
||||||
|
|
||||||
|
function walk(o, n, path = "") {
|
||||||
|
const keys = new Set([
|
||||||
|
...Object.keys(o || {}),
|
||||||
|
...Object.keys(n || {})
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const oldVal = o?.[key];
|
||||||
|
const newVal = n?.[key];
|
||||||
|
|
||||||
|
const currentPath = path ? `${path}.${key}` : key;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof oldVal === "object" &&
|
||||||
|
oldVal !== null &&
|
||||||
|
typeof newVal === "object" &&
|
||||||
|
newVal !== null &&
|
||||||
|
!Array.isArray(oldVal)
|
||||||
|
) {
|
||||||
|
walk(oldVal, newVal, currentPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
||||||
|
changes[currentPath] = newVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(oldObj, newObj);
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushHistory() {
|
||||||
|
history.push(clone(data));
|
||||||
|
redoStack.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function undo() {
|
||||||
|
if (!history.length) return;
|
||||||
|
redoStack.push(clone(data));
|
||||||
|
data = history.pop();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function redo() {
|
||||||
|
if (!redoStack.length) return;
|
||||||
|
history.push(clone(data));
|
||||||
|
data = redoStack.pop();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
if (onSave) onSave(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoResize(input) {
|
||||||
|
input.style.width = "10px";
|
||||||
|
input.style.width = input.scrollWidth + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(key, parent) {
|
||||||
|
if (key === null || parent == null) return;
|
||||||
|
|
||||||
|
if (!confirm("Wirklich löschen?")) return;
|
||||||
|
|
||||||
|
pushHistory();
|
||||||
|
|
||||||
|
if (Array.isArray(parent)) {
|
||||||
|
parent.splice(key, 1);
|
||||||
|
} else {
|
||||||
|
delete parent[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(data);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
container.innerHTML = "";
|
||||||
|
|
||||||
|
const controls = document.createElement("div");
|
||||||
|
controls.className = "json-controls";
|
||||||
|
|
||||||
|
const undoBtn = document.createElement("button");
|
||||||
|
undoBtn.className = "monolyth";
|
||||||
|
undoBtn.textContent = "Undo";
|
||||||
|
undoBtn.onclick = undo;
|
||||||
|
|
||||||
|
const redoBtn = document.createElement("button");
|
||||||
|
redoBtn.className = "monolyth";
|
||||||
|
redoBtn.textContent = "Redo";
|
||||||
|
redoBtn.onclick = redo;
|
||||||
|
|
||||||
|
controls.appendChild(undoBtn);
|
||||||
|
controls.appendChild(redoBtn);
|
||||||
|
|
||||||
|
if (onSave) {
|
||||||
|
const saveBtn = document.createElement("button");
|
||||||
|
saveBtn.className = "monolyth";
|
||||||
|
saveBtn.textContent = "Save";
|
||||||
|
saveBtn.onclick = save;
|
||||||
|
controls.appendChild(saveBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(controls);
|
||||||
|
container.appendChild(renderNode(data, null, null, "root", 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNode(value, key, parent, path, level) {
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "json-line";
|
||||||
|
wrapper.style.marginLeft = `${level * 18}px`;
|
||||||
|
|
||||||
|
const keySpan = document.createElement("span");
|
||||||
|
keySpan.className = "json-key";
|
||||||
|
|
||||||
|
if (key !== null) {
|
||||||
|
keySpan.textContent = `"${key}": `;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeBtn = document.createElement("span");
|
||||||
|
removeBtn.className = "json-remove";
|
||||||
|
removeBtn.textContent = " [x]";
|
||||||
|
removeBtn.onclick = (evt) => {
|
||||||
|
evt.stopPropagation();
|
||||||
|
remove(key, parent);
|
||||||
|
};
|
||||||
|
|
||||||
|
// OBJECT / ARRAY
|
||||||
|
if (typeof value === "object" && value !== null) {
|
||||||
|
const isArray = Array.isArray(value);
|
||||||
|
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.className = "json-header";
|
||||||
|
|
||||||
|
const toggle = document.createElement("span");
|
||||||
|
toggle.className = "json-toggle";
|
||||||
|
toggle.textContent = isArray ? "[ ]" : "{ }";
|
||||||
|
|
||||||
|
const keyLabel = document.createElement("span");
|
||||||
|
keyLabel.className = "json-key";
|
||||||
|
if (key !== null) keyLabel.textContent = `"${key}": `;
|
||||||
|
|
||||||
|
const addBtn = document.createElement("span");
|
||||||
|
addBtn.className = "json-add";
|
||||||
|
addBtn.textContent = " [+]";
|
||||||
|
|
||||||
|
const children = document.createElement("div");
|
||||||
|
children.className = "json-children";
|
||||||
|
|
||||||
|
keyLabel.onclick = toggle.onclick = () => {
|
||||||
|
children.classList.toggle("collapsed");
|
||||||
|
};
|
||||||
|
|
||||||
|
addBtn.onclick = () => {
|
||||||
|
let newKey = "";
|
||||||
|
let newValue;
|
||||||
|
|
||||||
|
feedbox({
|
||||||
|
title: `<span style="color:#f44336">Key</span> hinzufügen`,
|
||||||
|
message: `
|
||||||
|
<div style="display:flex;flex-direction:column;gap:8px;">
|
||||||
|
${!isArray ? `
|
||||||
|
<input id="newJsonKeyName" placeholder="Key Name" style="padding:6px;" />
|
||||||
|
` : ``}
|
||||||
|
<select id="newJsonValueType">
|
||||||
|
<option value="text">Text</option>
|
||||||
|
<option value="number">Zahl</option>
|
||||||
|
<option value="boolean">Boolean</option>
|
||||||
|
<option value="object">Objekt</option>
|
||||||
|
<option value="array">Array</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
buttons: {
|
||||||
|
yes: {
|
||||||
|
text: '<b>Anlegen</b>',
|
||||||
|
onClick: () => {
|
||||||
|
pushHistory();
|
||||||
|
|
||||||
|
if (!isArray) {
|
||||||
|
const input = document.querySelector('#newJsonKeyName');
|
||||||
|
newKey = input?.value?.trim();
|
||||||
|
|
||||||
|
if (!newKey) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedType = document.querySelector('#newJsonValueType').value;
|
||||||
|
|
||||||
|
switch (selectedType) {
|
||||||
|
case "number": newValue = 0; break;
|
||||||
|
case "boolean": newValue = false; break;
|
||||||
|
case "object": newValue = {}; break;
|
||||||
|
case "array": newValue = []; break;
|
||||||
|
default: newValue = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArray) {
|
||||||
|
value.push(newValue);
|
||||||
|
} else {
|
||||||
|
value[newKey] = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(data);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
no: { text: 'Abbrechen' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const inner = document.createElement("div");
|
||||||
|
|
||||||
|
const entries = isArray
|
||||||
|
? value.map((v, i) => [i, v])
|
||||||
|
: Object.entries(value);
|
||||||
|
|
||||||
|
entries.forEach(([k, v]) => {
|
||||||
|
inner.appendChild(
|
||||||
|
renderNode(v, k, value, `${path}.${k}`, level + 1)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
children.appendChild(inner);
|
||||||
|
|
||||||
|
header.appendChild(keyLabel);
|
||||||
|
header.appendChild(toggle);
|
||||||
|
header.appendChild(addBtn);
|
||||||
|
|
||||||
|
if (key !== null) header.appendChild(removeBtn);
|
||||||
|
|
||||||
|
wrapper.appendChild(header);
|
||||||
|
wrapper.appendChild(children);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
// STRING
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.className = "json-input string";
|
||||||
|
input.value = value;
|
||||||
|
|
||||||
|
input.oninput = () => {
|
||||||
|
pushHistory();
|
||||||
|
parent[key] = input.value;
|
||||||
|
autoResize(input);
|
||||||
|
onChange(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => autoResize(input), 0);
|
||||||
|
|
||||||
|
wrapper.appendChild(keySpan);
|
||||||
|
wrapper.appendChild(input);
|
||||||
|
if (key !== null) wrapper.appendChild(removeBtn);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NUMBER
|
||||||
|
if (typeof value === "number") {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "number";
|
||||||
|
input.className = "json-input number";
|
||||||
|
input.value = value;
|
||||||
|
|
||||||
|
input.oninput = () => {
|
||||||
|
pushHistory();
|
||||||
|
parent[key] = Number(input.value);
|
||||||
|
autoResize(input);
|
||||||
|
onChange(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => autoResize(input), 0);
|
||||||
|
|
||||||
|
wrapper.appendChild(keySpan);
|
||||||
|
wrapper.appendChild(input);
|
||||||
|
if (key !== null) wrapper.appendChild(removeBtn);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BOOLEAN
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "checkbox";
|
||||||
|
input.checked = value;
|
||||||
|
|
||||||
|
input.onchange = () => {
|
||||||
|
pushHistory();
|
||||||
|
parent[key] = input.checked;
|
||||||
|
onChange(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
wrapper.appendChild(keySpan);
|
||||||
|
wrapper.appendChild(input);
|
||||||
|
if (key !== null) wrapper.appendChild(removeBtn);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.textContent = String(value);
|
||||||
|
|
||||||
|
wrapper.appendChild(keySpan);
|
||||||
|
wrapper.appendChild(span);
|
||||||
|
if (key !== null) wrapper.appendChild(removeBtn);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushHistory();
|
||||||
|
render();
|
||||||
|
|
||||||
|
return {
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
save,
|
||||||
|
refresh: render,
|
||||||
|
getData: () => data,
|
||||||
|
getChanges() {
|
||||||
|
return diff(lastSnapshot, data);
|
||||||
|
},
|
||||||
|
commit() {
|
||||||
|
lastSnapshot = clone(data);
|
||||||
|
},
|
||||||
|
update(newData) {
|
||||||
|
data = newData;
|
||||||
|
lastSnapshot = clone(newData);
|
||||||
|
pushHistory();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
244
public/javascript/contextMenu.js
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
const ctx = new ContextMenu();
|
||||||
|
|
||||||
|
ctx.setItems([
|
||||||
|
{ label: "Öffnen", disable: true, onClick: () => alert("Öffnen!") },
|
||||||
|
{ label: "Öffnen", onClick: () => alert("Öffnen!") },
|
||||||
|
{ label: "Bearbeiten", onClick: () => alert("Bearbeiten…") },
|
||||||
|
{ type: "divider" },
|
||||||
|
{
|
||||||
|
label: "Mehr Optionen",
|
||||||
|
color: 'rgb()|color-name|color-value',
|
||||||
|
children: [
|
||||||
|
{ label: "Duplizieren", onClick: () => alert("Dupliziert!") },
|
||||||
|
{ label: "Umbenennen", onClick: () => alert("Rename…") },
|
||||||
|
{
|
||||||
|
label: "Exportieren",
|
||||||
|
children: [
|
||||||
|
{ label: "PDF", onClick: () => alert("PDF export") },
|
||||||
|
{ label: "CSV", onClick: () => alert("CSV export") },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ label: "Löschen", onClick: () => alert("Gelöscht!") },
|
||||||
|
]);
|
||||||
|
|
||||||
|
ctx.show(element, or location, { position: "right" });
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
class ContextMenu {
|
||||||
|
constructor() {
|
||||||
|
this.menu = document.createElement("div");
|
||||||
|
this.menu.className = "ctx-menu hidden";
|
||||||
|
document.body.appendChild(this.menu);
|
||||||
|
|
||||||
|
// close on click outside → sofort
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (!this.menu.contains(e.target)) {
|
||||||
|
this.closeAllSubmenus();
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => this.hide());
|
||||||
|
window.addEventListener("scroll", () => this.hide());
|
||||||
|
}
|
||||||
|
|
||||||
|
setItems(items) {
|
||||||
|
this.menu.innerHTML = "";
|
||||||
|
this.menu.appendChild(this.buildMenu(items));
|
||||||
|
}
|
||||||
|
|
||||||
|
buildMenu(items) {
|
||||||
|
const ul = document.createElement("ul");
|
||||||
|
ul.className = "ctx-list";
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.type === "divider") {
|
||||||
|
const divider = document.createElement("li");
|
||||||
|
divider.className = "ctx-divider";
|
||||||
|
ul.appendChild(divider);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.className = "ctx-item";
|
||||||
|
|
||||||
|
if (item.color) li.style.boxShadow = `inset 5px 0px 0px 0px ${item.color}`;
|
||||||
|
if (item.disabled) li.classList.add("ctx-disabled");
|
||||||
|
|
||||||
|
li.innerHTML = `
|
||||||
|
<span class="ctx-label">${item.label}</span>
|
||||||
|
${item.children ? "<span class='ctx-arrow'>▶</span>" : ""}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (item.onClick && !item.children && !item.disabled) {
|
||||||
|
li.addEventListener("click", e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
item.onClick(e);
|
||||||
|
this.hide(); // Hauptmenü sofort schließen
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.children) {
|
||||||
|
const submenu = this.buildMenu(item.children);
|
||||||
|
submenu.classList.add("ctx-submenu");
|
||||||
|
li.appendChild(submenu);
|
||||||
|
|
||||||
|
let hideTimeout;
|
||||||
|
|
||||||
|
const openSubmenu = () => {
|
||||||
|
clearTimeout(hideTimeout);
|
||||||
|
submenu.classList.add("open");
|
||||||
|
|
||||||
|
submenu.style.left = "";
|
||||||
|
submenu.style.top = "";
|
||||||
|
|
||||||
|
const liRect = li.getBoundingClientRect();
|
||||||
|
const submenuRect = submenu.getBoundingClientRect();
|
||||||
|
|
||||||
|
let left = li.offsetWidth;
|
||||||
|
if (liRect.right + submenuRect.width > window.innerWidth) left = -submenuRect.width;
|
||||||
|
submenu.style.left = left + "px";
|
||||||
|
|
||||||
|
let top = 0;
|
||||||
|
if (liRect.top + submenuRect.height > window.innerHeight) {
|
||||||
|
top = window.innerHeight - (liRect.top + submenuRect.height) - 4;
|
||||||
|
}
|
||||||
|
submenu.style.top = top + "px";
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeSubmenu = (delay = 500) => {
|
||||||
|
clearTimeout(hideTimeout);
|
||||||
|
hideTimeout = setTimeout(() => {
|
||||||
|
submenu.classList.remove("open");
|
||||||
|
submenu.style.left = "";
|
||||||
|
submenu.style.top = "";
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
|
||||||
|
li.addEventListener("mouseenter", openSubmenu);
|
||||||
|
li.addEventListener("mouseleave", () => closeSubmenu(500));
|
||||||
|
submenu.addEventListener("mouseenter", () => clearTimeout(hideTimeout));
|
||||||
|
submenu.addEventListener("mouseleave", () => closeSubmenu(500));
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
return ul;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAllSubmenus() {
|
||||||
|
this.menu.querySelectorAll(".ctx-submenu.open").forEach(sm => {
|
||||||
|
sm.classList.remove("open");
|
||||||
|
sm.style.left = "";
|
||||||
|
sm.style.top = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
show(target, options = {}) {
|
||||||
|
this.closeAllSubmenus();
|
||||||
|
|
||||||
|
let x, y;
|
||||||
|
|
||||||
|
const position = options.position || "right"; // default
|
||||||
|
const offset = options.offset || 4;
|
||||||
|
|
||||||
|
// 🖱️ Mausposition
|
||||||
|
if (typeof target === "number") {
|
||||||
|
x = target;
|
||||||
|
y = options.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📦 Element als Anchor
|
||||||
|
else if (target instanceof HTMLElement) {
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
|
||||||
|
switch (position) {
|
||||||
|
case "right":
|
||||||
|
x = rect.right + offset;
|
||||||
|
y = rect.top;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "left":
|
||||||
|
x = rect.left - this.menu.offsetWidth - offset;
|
||||||
|
y = rect.top;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "top":
|
||||||
|
x = rect.left;
|
||||||
|
y = rect.top - this.menu.offsetHeight - offset;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "bottom":
|
||||||
|
default:
|
||||||
|
x = rect.left;
|
||||||
|
y = rect.bottom + offset;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ invalid fallback
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 👉 erstmal anzeigen (wichtig für width/height!)
|
||||||
|
this.menu.style.left = "0px";
|
||||||
|
this.menu.style.top = "0px";
|
||||||
|
this.menu.classList.add("show");
|
||||||
|
this.menu.classList.remove("hidden");
|
||||||
|
|
||||||
|
const rect = this.menu.getBoundingClientRect();
|
||||||
|
|
||||||
|
// 🧠 Reposition nach echten Maßen
|
||||||
|
if (target instanceof HTMLElement) {
|
||||||
|
const anchor = target.getBoundingClientRect();
|
||||||
|
|
||||||
|
switch (position) {
|
||||||
|
case "right":
|
||||||
|
x = anchor.right + offset;
|
||||||
|
y = anchor.top;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "left":
|
||||||
|
x = anchor.left - rect.width - offset;
|
||||||
|
y = anchor.top;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "top":
|
||||||
|
x = anchor.left;
|
||||||
|
y = anchor.top - rect.height - offset;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "bottom":
|
||||||
|
x = anchor.left;
|
||||||
|
y = anchor.bottom + offset;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧱 Screen Bounds
|
||||||
|
if (x + rect.width > window.innerWidth) {
|
||||||
|
x = window.innerWidth - rect.width - 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y + rect.height > window.innerHeight) {
|
||||||
|
y = window.innerHeight - rect.height - 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x < 0) x = 4;
|
||||||
|
if (y < 0) y = 4;
|
||||||
|
|
||||||
|
this.menu.style.left = x + "px";
|
||||||
|
this.menu.style.top = y + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.menu.classList.remove("show");
|
||||||
|
setTimeout(() => this.menu.classList.add("hidden"), 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
259
public/javascript/customModal.js
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
let activeFeedbox = null;
|
||||||
|
|
||||||
|
|
||||||
|
//#region Messagebox
|
||||||
|
function getMessageColorByLevelId(levelId) {
|
||||||
|
return levelId == -1 ? 'test' :
|
||||||
|
levelId == 0 ? 'success' :
|
||||||
|
levelId == 1 ? 'info' :
|
||||||
|
levelId == 2 ? 'warn' :
|
||||||
|
levelId == 4 ? 'error' : 'throw_exception';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows messages in a containerbox on the right
|
||||||
|
* @param {string} title - Title
|
||||||
|
* @param {string} text - Message content
|
||||||
|
* @param {string} levelId - 1=info | 2=warn | 4=error | 8=throw_exception | 16=success
|
||||||
|
* @param {string} [targetId] - Which element is getting focused
|
||||||
|
* @param {number} duration - Time in ms until auto-close
|
||||||
|
*/
|
||||||
|
// function showMessage(title, text, levelId = 1, targetId = null, duration = 4000) {
|
||||||
|
function showMessage(title, text, levelId = 1, onclick = null, duration = 4000) {
|
||||||
|
// Falls kein Container existiert → automatisch anlegen
|
||||||
|
let container = document.getElementById('message-container');
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement('div');
|
||||||
|
container.id = 'message-container';
|
||||||
|
container.style.position = 'fixed';
|
||||||
|
container.style.top = '20px';
|
||||||
|
container.style.right = '20px';
|
||||||
|
container.style.display = 'flex';
|
||||||
|
container.style.flexDirection = 'column';
|
||||||
|
container.style.alignItems = 'flex-end';
|
||||||
|
container.style.zIndex = '9999';
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
const message = document.createElement('div');
|
||||||
|
message.classList.add('message', getMessageColorByLevelId(levelId));
|
||||||
|
|
||||||
|
// --- HEADER ---
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'message-header';
|
||||||
|
|
||||||
|
const titleContainer = document.createElement('div');
|
||||||
|
titleContainer.className = 'message-title';
|
||||||
|
titleContainer.innerHTML = title;
|
||||||
|
|
||||||
|
const pinDiv = document.createElement('div');
|
||||||
|
pinDiv.className = 'pin-div';
|
||||||
|
pinDiv.innerHTML = '📌';
|
||||||
|
pinDiv.title = 'Anpinnen';
|
||||||
|
|
||||||
|
const countdown = document.createElement('span');
|
||||||
|
countdown.className = 'countdown';
|
||||||
|
countdown.textContent = `${(duration / 1000).toFixed(1)}s`;
|
||||||
|
|
||||||
|
// Titel + Countdown + Pin in eine Zeile
|
||||||
|
header.appendChild(titleContainer);
|
||||||
|
header.appendChild(countdown);
|
||||||
|
header.appendChild(pinDiv);
|
||||||
|
|
||||||
|
// --- BODY ---
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'message-text';
|
||||||
|
body.innerHTML = text;
|
||||||
|
|
||||||
|
message.appendChild(header);
|
||||||
|
message.appendChild(body);
|
||||||
|
container.appendChild(message);
|
||||||
|
|
||||||
|
// --- LOGIK ---
|
||||||
|
let pinned = false;
|
||||||
|
let remainingTime = duration;
|
||||||
|
let interval = null;
|
||||||
|
let lastTick = Date.now();
|
||||||
|
|
||||||
|
function startCountdown() {
|
||||||
|
lastTick = Date.now();
|
||||||
|
clearInterval(interval);
|
||||||
|
interval = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
const delta = now - lastTick;
|
||||||
|
lastTick = now;
|
||||||
|
remainingTime -= delta;
|
||||||
|
|
||||||
|
if (remainingTime <= 0 && !pinned) {
|
||||||
|
clearInterval(interval);
|
||||||
|
slideOutMessage(message);
|
||||||
|
} else if (!pinned) {
|
||||||
|
countdown.textContent = `${(remainingTime / 1000).toFixed(1)}s`;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopCountdown() {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start Countdown
|
||||||
|
startCountdown();
|
||||||
|
|
||||||
|
// Hover pausiert Countdown
|
||||||
|
message.addEventListener('mouseenter', stopCountdown);
|
||||||
|
message.addEventListener('mouseleave', () => {
|
||||||
|
if (!pinned) startCountdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Klick auf Nachricht → schließen (nur wenn nicht gepinnt)
|
||||||
|
message.addEventListener('click', (e) => {
|
||||||
|
if(onclick) {
|
||||||
|
onclick();
|
||||||
|
}
|
||||||
|
// if (e.target === pinDiv) return;
|
||||||
|
// if (targetId) {
|
||||||
|
// const target = document.getElementById(targetId);
|
||||||
|
// if (target) target.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
// }
|
||||||
|
if (!pinned) slideOutMessage(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Klick auf Pin → anheften / lösen
|
||||||
|
pinDiv.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
pinned = !pinned;
|
||||||
|
if (pinned) {
|
||||||
|
pinDiv.classList.add('pinned');
|
||||||
|
pinDiv.title = 'Loslösen';
|
||||||
|
stopCountdown();
|
||||||
|
countdown.textContent = '📍 Angeheftet';
|
||||||
|
} else {
|
||||||
|
pinDiv.classList.remove('pinned');
|
||||||
|
pinDiv.title = 'Anpinnen';
|
||||||
|
startCountdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function slideOutMessage(message) {
|
||||||
|
if (!message) return;
|
||||||
|
message.style.animation = 'slideOut 0.5s forwards';
|
||||||
|
setTimeout(() => {
|
||||||
|
if (message.parentNode) message.parentNode.removeChild(message);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//#region Feedbox
|
||||||
|
|
||||||
|
/**
|
||||||
|
* feedbox({
|
||||||
|
* title: `<span style="color:#f44336">⚠ Upload abbrechen?</span>`,
|
||||||
|
* message: `
|
||||||
|
* <p>Es laufen noch <b>aktive Uploads</b>.</p>
|
||||||
|
* <p>Möchtest du wirklich <u>alle abbrechen</u>?</p>
|
||||||
|
* `,
|
||||||
|
* buttons: {
|
||||||
|
* yes: {
|
||||||
|
* text: '<b>Ja</b>, abbrechen',
|
||||||
|
* onClick: () => stopUploadQueue()
|
||||||
|
* },
|
||||||
|
* no: {
|
||||||
|
* text: 'Weiter hochladen'
|
||||||
|
* },
|
||||||
|
* cancel: {
|
||||||
|
* text: 'Zurück'
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*});
|
||||||
|
*/
|
||||||
|
function feedbox({
|
||||||
|
title = '',
|
||||||
|
message = '',
|
||||||
|
buttons = {}, // buttons: { yes: { text: 'Yes', onClick: () => { } } }
|
||||||
|
primary = null, // name of the primary button, to accept with enter-key
|
||||||
|
lock = false, // locks desktop
|
||||||
|
replace = false // replaces an actually shown feedbox
|
||||||
|
}) {
|
||||||
|
// 🚫 Nested verhindern
|
||||||
|
if (activeFeedbox) {
|
||||||
|
if (!replace) {
|
||||||
|
return Promise.resolve('blocked');
|
||||||
|
}
|
||||||
|
activeFeedbox.close('replaced');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'feedbox-overlay';
|
||||||
|
|
||||||
|
const box = document.createElement('div');
|
||||||
|
box.className = 'feedbox';
|
||||||
|
|
||||||
|
const h = document.createElement('h3');
|
||||||
|
h.innerHTML = title;
|
||||||
|
|
||||||
|
const msg = document.createElement('div');
|
||||||
|
msg.className = 'feedbox-message';
|
||||||
|
msg.innerHTML = message;
|
||||||
|
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'feedbox-actions';
|
||||||
|
|
||||||
|
const btnMap = {};
|
||||||
|
|
||||||
|
Object.entries(buttons).forEach(([key, cfg]) => {
|
||||||
|
if (!cfg) return;
|
||||||
|
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = `feedbox-btn ${key}`;
|
||||||
|
btn.innerHTML = cfg.text ?? key;
|
||||||
|
|
||||||
|
btn.onclick = () => {
|
||||||
|
cfg.onClick?.();
|
||||||
|
close(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
btnMap[key] = btn;
|
||||||
|
actions.appendChild(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
function close(result) {
|
||||||
|
document.removeEventListener('keydown', keyHandler);
|
||||||
|
overlay.remove();
|
||||||
|
activeFeedbox = null;
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyHandler(e) {
|
||||||
|
if (e.key === 'Escape') close('cancel');
|
||||||
|
if (e.key === 'Enter' && primary && btnMap[primary]) {
|
||||||
|
btnMap[primary].click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lock) {
|
||||||
|
overlay.onclick = e => {
|
||||||
|
if (e.target === overlay) close('cancel');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', keyHandler);
|
||||||
|
|
||||||
|
box.append(h, msg, actions);
|
||||||
|
overlay.appendChild(box);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
// Fokus
|
||||||
|
if (primary && btnMap[primary]) {
|
||||||
|
setTimeout(() => btnMap[primary].focus(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔐 Singleton setzen
|
||||||
|
activeFeedbox = { close };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
97
public/javascript/loadOnce.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
const auth = { objectGuid: getCookie('ObjectGUID'), sAMAccountName: getCookie('sAMAccountName') };
|
||||||
|
const mainSocket = io.connect('/', { reconnect: true, auth: auth });
|
||||||
|
const adminSocket = io.connect('/admin', { reconnect: true, auth: auth });
|
||||||
|
|
||||||
|
const container = document.getElementById('message-container');
|
||||||
|
|
||||||
|
|
||||||
|
adminSocket.on('eventlog', (data) => {
|
||||||
|
showMessage('EventLog',
|
||||||
|
`${data.datetime}<br>${[-1,0].includes(data.levelId) ? '' : data.trace}<br><br>${(Array.isArray(data.message) ? data.message.split('\r\n').join('<br>').split('\t').join(' ') : data.message.split('\r\n').join('<br>').split('\t').join(' '))}`,
|
||||||
|
data.levelId,
|
||||||
|
() => {
|
||||||
|
|
||||||
|
} ,
|
||||||
|
10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
mainSocket.on('event', (data) => {
|
||||||
|
showMessage(data.pluginName,
|
||||||
|
`[${data.datetime}]<br><br>${(Array.isArray(data.message) ? data.message.split('\r\n').join('<br>').split('\t').join(' ') : data.message.split('\r\n').join('<br>').split('\t').join(' '))}`,
|
||||||
|
data.levelId,
|
||||||
|
() => {
|
||||||
|
|
||||||
|
} ,
|
||||||
|
10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//-1=test, 0=success, 1=log, 2=warn, 4=error, 8=throw_exception
|
||||||
|
function writeEventLog(levelId, pluginName, message) {
|
||||||
|
adminSocket.emit('eventlog', {
|
||||||
|
objectGuid: getCookie('ObjectGUID'),
|
||||||
|
levelId: levelId,
|
||||||
|
pluginName: pluginName,
|
||||||
|
message: message.stack === undefined ? message : { message: message.message, stack: message.stack }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// levelId: if -1, then write no log entry
|
||||||
|
// sendToParams: where clause to find objectGUIDs to send
|
||||||
|
function sendUserEvent(pluginName, message, sendToParams, levelId = -1) {
|
||||||
|
mainSocket.emit('event', {
|
||||||
|
objectGuid: getCookie('ObjectGUID'),
|
||||||
|
levelId: levelId,
|
||||||
|
pluginName: pluginName,
|
||||||
|
message: message.stack === undefined ? message : { message: message.message },
|
||||||
|
sendToParams: sendToParams
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"status":"unload",
|
||||||
|
"pluginName":"user_management",
|
||||||
|
"metadata":{
|
||||||
|
"name":"user_management",
|
||||||
|
"menuName":"User Management",
|
||||||
|
"description":"Beschreibung hier einfügen",
|
||||||
|
"version":"0.9.25.11.14",
|
||||||
|
"icon":"",
|
||||||
|
"permissions":[],
|
||||||
|
"config":{
|
||||||
|
},
|
||||||
|
"active":false
|
||||||
|
},
|
||||||
|
"levelId":0,
|
||||||
|
"message":"Plugin user_management entladen",
|
||||||
|
"authorized": true
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
mainSocket.on('plugin_status', payload => {
|
||||||
|
const startMenuItem = document.querySelector(`[data-appname=${payload.metadata.name}]`);
|
||||||
|
if(['load', 'unload', 'update'].includes(payload.status)) {
|
||||||
|
if(payload.status == 'load') {
|
||||||
|
if(payload.authorized) {
|
||||||
|
startMenuItem.classList.remove('unload');
|
||||||
|
startMenuItem.setAttribute('data-active', true);
|
||||||
|
} else {
|
||||||
|
startMenuItem.classList.add('unload');
|
||||||
|
startMenuItem.setAttribute('data-active', false);
|
||||||
|
}
|
||||||
|
} else if(payload.status == 'unload') {
|
||||||
|
startMenuItem.classList.add('unload');
|
||||||
|
startMenuItem.setAttribute('data-active', false);
|
||||||
|
} else if(payload.status == 'update') {
|
||||||
|
if(payload.authorized && payload.metadata.active) {
|
||||||
|
startMenuItem.classList.remove('unload');
|
||||||
|
startMenuItem.setAttribute('data-active', true);
|
||||||
|
} else if(!payload.authorized){
|
||||||
|
startMenuItem.classList.add('unload');
|
||||||
|
startMenuItem.setAttribute('data-active', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
1494
public/javascript/main.js
Normal file
157
public/javascript/notifyBubble.js
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
class NotifyBubble {
|
||||||
|
constructor(trayButton, bubbleSelector) {
|
||||||
|
this.button = trayButton;
|
||||||
|
this.bubble = document.querySelector(bubbleSelector);
|
||||||
|
this.hideTimeout = null;
|
||||||
|
this.initEvents();
|
||||||
|
this.initExistingItems();
|
||||||
|
this.updateCounter(); // 🔥 initialer Counter
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
SHOW / HIDE
|
||||||
|
========================== */
|
||||||
|
show() {
|
||||||
|
clearTimeout(this.hideTimeout);
|
||||||
|
this.button.classList.add("active");
|
||||||
|
|
||||||
|
// Tooltip entfernen beim Öffnen
|
||||||
|
this.button.removeAttribute('data-tooltip');
|
||||||
|
}
|
||||||
|
|
||||||
|
hide(delay = 1000) {
|
||||||
|
clearTimeout(this.hideTimeout);
|
||||||
|
|
||||||
|
this.hideTimeout = setTimeout(() => {
|
||||||
|
this.button.classList.remove("active");
|
||||||
|
|
||||||
|
// 🔥 Counter nach Schließen anzeigen
|
||||||
|
this.updateCounter();
|
||||||
|
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
COUNTER / TOOLTIP
|
||||||
|
========================== */
|
||||||
|
updateCounter() {
|
||||||
|
const items = this.bubble.querySelectorAll(".bubble-item");
|
||||||
|
const count = items.length;
|
||||||
|
|
||||||
|
if (!this.button.classList.contains("active") && count > 0) {
|
||||||
|
this.button.setAttribute('data-tooltip', `${count} neue Benachrichtigung${count > 1 ? 'en' : ''}`);
|
||||||
|
} else {
|
||||||
|
this.button.setAttribute('data-tooltip', "Keine Benachrichtigungen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
EVENTS
|
||||||
|
========================== */
|
||||||
|
initEvents() {
|
||||||
|
// Toggle bei Klick auf Button
|
||||||
|
this.button.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (this.button.classList.contains("active")) {
|
||||||
|
this.hide(0);
|
||||||
|
} else {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Klick innerhalb der Bubble soll NICHT schließen
|
||||||
|
this.bubble.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Klick irgendwo anders → schließen
|
||||||
|
document.addEventListener("click", () => {
|
||||||
|
if (this.button.classList.contains("active")) {
|
||||||
|
this.hide(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
INIT EXISTING ITEMS
|
||||||
|
========================== */
|
||||||
|
initExistingItems() {
|
||||||
|
this.bubble.querySelectorAll(".bubble-item").forEach(item => {
|
||||||
|
this.attachItemEvents(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
ITEM EVENTS
|
||||||
|
========================== */
|
||||||
|
attachItemEvents(item) {
|
||||||
|
item.addEventListener("click", () => {
|
||||||
|
const checkbox = item.querySelector("input[type='checkbox']");
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.checked = !checkbox.checked;
|
||||||
|
|
||||||
|
fetch('/api/NotifyTray/markAsSeen', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: item.dataset.id,
|
||||||
|
value: checkbox.checked
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔥 Counter ggf. aktualisieren
|
||||||
|
this.updateCounter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
ADD SINGLE ITEM
|
||||||
|
========================== */
|
||||||
|
addItem(notification) {
|
||||||
|
const item = document.createElement("label");
|
||||||
|
item.className = "bubble-item";
|
||||||
|
item.dataset.id = notification.ID;
|
||||||
|
|
||||||
|
item.innerHTML = `
|
||||||
|
<div>${notification.Message}</div>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-direction:row;flex-wrap:no-wrap;justify-content:flex-end;gap:10px;align-items:center;">
|
||||||
|
<div>
|
||||||
|
<div>${dateFormat(notification.CreatedAt, 'dd.mm.yyyy HH:MM:SS')}</div>
|
||||||
|
<div>${notification.PluginName}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="cb cb-modern">
|
||||||
|
<input id="notification_${notification.ID}" type="checkbox">
|
||||||
|
<span class="cb-box"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.attachItemEvents(item);
|
||||||
|
this.bubble.appendChild(item);
|
||||||
|
|
||||||
|
// 🔥 Counter aktualisieren
|
||||||
|
this.updateCounter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
ADD MULTIPLE ITEMS
|
||||||
|
========================== */
|
||||||
|
addItems(notifications) {
|
||||||
|
notifications.forEach(n => this.addItem(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
CLEAR
|
||||||
|
========================== */
|
||||||
|
clear() {
|
||||||
|
this.bubble.innerHTML = "";
|
||||||
|
this.updateCounter();
|
||||||
|
}
|
||||||
|
}
|
||||||
1081
public/javascript/os.js
Normal file
92
public/javascript/pluginAPI.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
const pluginAPI = {
|
||||||
|
async update(name, updates) {
|
||||||
|
if(Object.keys(updates)[0] && Object.values(updates)[0] == "") {
|
||||||
|
updates[Object.keys(updates)[0]] = [ ];
|
||||||
|
}
|
||||||
|
return await this._request(
|
||||||
|
`/api/plugins/${name}/update`,
|
||||||
|
'POST',
|
||||||
|
{ updates }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async activation(name, state) {
|
||||||
|
return await this._request(
|
||||||
|
`/api/plugins/activation`,
|
||||||
|
'POST',
|
||||||
|
{ name, state }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async create(name) {
|
||||||
|
return await this._request(
|
||||||
|
`/api/plugins/${name}/create`,
|
||||||
|
'POST'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async rename(name, newName) {
|
||||||
|
return await this._request(
|
||||||
|
`/api/plugins/${name}/rename`,
|
||||||
|
'POST',
|
||||||
|
{ newName }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
async delete(name) {
|
||||||
|
return await this._request(
|
||||||
|
`/api/plugins/${name}/delete`,
|
||||||
|
'POST'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- zentrale Request-Funktion ---
|
||||||
|
async _request(url, method, body) {
|
||||||
|
try {
|
||||||
|
const options = { method };
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
options.headers = { 'Content-Type': 'application/json' };
|
||||||
|
options.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url, options);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
writeEventLog(4, 'CLIENT', `Request fehlgeschlagen: ${text}`);
|
||||||
|
throw new Error(`HTTP ${res.status}: ${text}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
} catch (err) {
|
||||||
|
writeEventLog(4, 'CLIENT', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AttachOnBlurChange {
|
||||||
|
constructor(el, callback) {
|
||||||
|
this.el = el;
|
||||||
|
this.callback = callback;
|
||||||
|
this.initial = this.el.value;
|
||||||
|
|
||||||
|
this.el.addEventListener('blur', () => {
|
||||||
|
this.handleBlur();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlur() {
|
||||||
|
if (this.el.value === this.initial) return;
|
||||||
|
|
||||||
|
const oldValue = this.initial;
|
||||||
|
const newValue = this.el.value;
|
||||||
|
|
||||||
|
this.initial = newValue;
|
||||||
|
|
||||||
|
this.callback(newValue, oldValue, this.el);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
public/javascript/requiredFields.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
function createRequiredProgress({ container, progress, onChange }) {
|
||||||
|
let total = 0;
|
||||||
|
let filled = 0;
|
||||||
|
|
||||||
|
const body = typeof container === 'string'
|
||||||
|
? document.querySelector(container)
|
||||||
|
: container;
|
||||||
|
|
||||||
|
const progressEl = typeof progress === 'string'
|
||||||
|
? document.querySelector(progress)
|
||||||
|
: progress;
|
||||||
|
|
||||||
|
if (!body || !progressEl) return;
|
||||||
|
|
||||||
|
function isElementVisible(el) {
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
return el.offsetParent !== null && style.opacity !== '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRequiredProgress() {
|
||||||
|
const requiredElements = Array.from(
|
||||||
|
body.querySelectorAll('input[required], select[required], textarea[required]')
|
||||||
|
).filter(isElementVisible);
|
||||||
|
|
||||||
|
total = requiredElements.length;
|
||||||
|
filled = requiredElements.filter(el =>
|
||||||
|
el.classList.contains('is-required-filled')
|
||||||
|
).length;
|
||||||
|
|
||||||
|
progressEl.textContent = `${filled} / ${total} Pflichtfelder ausgefüllt`;
|
||||||
|
progressEl.classList.toggle('complete', filled === total);
|
||||||
|
progressEl.classList.toggle('incomplete', filled !== total);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRequiredState(el) {
|
||||||
|
const isEmpty =
|
||||||
|
(el instanceof HTMLSelectElement && !el.value) ||
|
||||||
|
(el instanceof HTMLInputElement && el.type !== 'checkbox' && !el.value.trim()) ||
|
||||||
|
(el instanceof HTMLTextAreaElement && !el.value.trim());
|
||||||
|
|
||||||
|
el.classList.toggle('is-required-empty', isEmpty);
|
||||||
|
el.classList.toggle('is-required-filled', !isEmpty);
|
||||||
|
|
||||||
|
updateRequiredProgress();
|
||||||
|
|
||||||
|
if (typeof onChange === 'function') {
|
||||||
|
onChange({
|
||||||
|
element: el,
|
||||||
|
isEmpty,
|
||||||
|
isFilled: !isEmpty,
|
||||||
|
isFinished: total === filled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialize() {
|
||||||
|
const elements = Array.from(
|
||||||
|
body.querySelectorAll('input[required], select[required], textarea[required]')
|
||||||
|
).filter(isElementVisible);
|
||||||
|
|
||||||
|
elements.forEach(el => {
|
||||||
|
updateRequiredState(el);
|
||||||
|
|
||||||
|
el.addEventListener('input', () => updateRequiredState(el));
|
||||||
|
el.addEventListener('change', () => updateRequiredState(el));
|
||||||
|
});
|
||||||
|
|
||||||
|
updateRequiredProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 Reagiere auf Sichtbarkeitsänderungen
|
||||||
|
const observer = new MutationObserver(updateRequiredProgress);
|
||||||
|
observer.observe(body, {
|
||||||
|
attributes: true,
|
||||||
|
subtree: true,
|
||||||
|
attributeFilter: ['style', 'class', 'hidden']
|
||||||
|
});
|
||||||
|
|
||||||
|
initialize();
|
||||||
|
|
||||||
|
// Optional: API zurückgeben
|
||||||
|
return {
|
||||||
|
initialize: initialize,
|
||||||
|
refresh: updateRequiredProgress,
|
||||||
|
destroy() {
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
450
public/javascript/tableFilter.js
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
/*
|
||||||
|
//Generic table filter module
|
||||||
|
// Usage:
|
||||||
|
TableFilter({
|
||||||
|
table: document.querySelector('#myTable'),
|
||||||
|
exceptedColumns: [ 'Status_ID' ],
|
||||||
|
filterConfig: {
|
||||||
|
columnModes: {
|
||||||
|
ID: 'text', // Textsuche in dieser Spalte
|
||||||
|
Status: 'dropdown', // Dropdown-Werte aus Tabelle sammeln
|
||||||
|
Objekt: 'text', // Textsuche
|
||||||
|
Priorität: 'dropdown',
|
||||||
|
Gewerk: 'dropdown',
|
||||||
|
Typ: 'dropdown',
|
||||||
|
Bedarfsmelder: 'text'
|
||||||
|
Bearbeiter: 'text'
|
||||||
|
Genehmiger: 'text'
|
||||||
|
},
|
||||||
|
checkboxFilter: {
|
||||||
|
column: 'Status_ID',
|
||||||
|
rules: [
|
||||||
|
{ label: 'Bearbeitung', test: v => [1,4,6,7,12,13,14].includes(parseInt(v)) },
|
||||||
|
{ label: 'Genehmigt', test: v => [2,5,9,10,11].includes(parseInt(v)) },
|
||||||
|
{ label: 'Abgelehnt', test: v => [3].includes(parseInt(v)) },
|
||||||
|
{ label: 'Abgeschlossen', test: v => [8].includes(parseInt(v)) }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Generic table filter module (one textbox OR column-based dropdown)
|
||||||
|
// New features:
|
||||||
|
// - Selecting a column in the column-selector decides: textbox OR dropdown (configured in options)
|
||||||
|
// - If column-selector = empty value ⇒ full-row search
|
||||||
|
// - No extra checkbox needed
|
||||||
|
// - UI stays compact (only ONE input element + ONE dropdown)
|
||||||
|
*/
|
||||||
|
function TableFilter(options) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const table = options.table;
|
||||||
|
const cfg = options.filterConfig;
|
||||||
|
const exceptedColumns = options.exceptedColumns || [];
|
||||||
|
|
||||||
|
// Gruppenerkennung + Verwaltung
|
||||||
|
const groupMap = new Map(); // groupRow → childRows[]
|
||||||
|
let groupsInitialized = false;
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
selectedColumn: '',
|
||||||
|
searchText: '',
|
||||||
|
dropdownValue: '',
|
||||||
|
dropdownCache: {},
|
||||||
|
checkbox: new Set(),
|
||||||
|
};
|
||||||
|
|
||||||
|
//------------------------------------
|
||||||
|
// Sticky Container für Filter + Header
|
||||||
|
//------------------------------------
|
||||||
|
if(document.querySelectorAll('.table-filter-container').length > 0) {
|
||||||
|
// Array.from(document.querySelectorAll('.table-filter-container')).forEach(filterContainer => {
|
||||||
|
// filterContainer.remove();
|
||||||
|
// })
|
||||||
|
const existing = table.previousElementSibling;
|
||||||
|
if (existing && existing.classList.contains('table-filter-container')) {
|
||||||
|
existing.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'table-filter-container';
|
||||||
|
container.style.position = 'sticky';
|
||||||
|
container.style.top = '0px';
|
||||||
|
container.style.left = '0px';
|
||||||
|
container.style.zIndex = 20;
|
||||||
|
|
||||||
|
// Filter UI wird hier reingesetzt
|
||||||
|
table.before(container);
|
||||||
|
|
||||||
|
// Tabelle selbst: header sticky
|
||||||
|
const thead = table.querySelector('thead');
|
||||||
|
|
||||||
|
|
||||||
|
const headerCells = Array.from(table.querySelectorAll('thead th'));
|
||||||
|
const getColumnIndex = name => headerCells.findIndex(c => c.textContent.trim() === name);
|
||||||
|
|
||||||
|
//------------------------------------
|
||||||
|
// Dynamisches Filter-UI
|
||||||
|
//------------------------------------
|
||||||
|
function createDynamicFilterUI() {
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.style="display:flex;flex-direction:row;align-items:center;gap:10px"
|
||||||
|
|
||||||
|
const colSelectLabel = document.createElement('label');
|
||||||
|
const colSelect = document.createElement('select');
|
||||||
|
const emptyOpt = document.createElement('option');
|
||||||
|
emptyOpt.value = '';
|
||||||
|
emptyOpt.textContent = '-- Alles --';
|
||||||
|
colSelect.appendChild(emptyOpt);
|
||||||
|
colSelectLabel.innerText = 'Spalte';
|
||||||
|
|
||||||
|
headerCells.forEach(cell => {
|
||||||
|
if(!exceptedColumns.includes(cell.textContent.trim())) {
|
||||||
|
const name = cell.textContent.trim();
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = name;
|
||||||
|
opt.textContent = name;
|
||||||
|
colSelect.appendChild(opt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'search';
|
||||||
|
input.style.display = 'none';
|
||||||
|
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.style.display = 'none';
|
||||||
|
select.innerHTML = '<option value="">-- Alle --</option>';
|
||||||
|
|
||||||
|
function populateDropdownForColumn(colName) {
|
||||||
|
const colIndex = getColumnIndex(colName);
|
||||||
|
const rows = Array.from(table.querySelectorAll('tbody tr')).filter(r => !r.classList.contains('grouprow'));
|
||||||
|
const values = new Set();
|
||||||
|
rows.forEach(r => {
|
||||||
|
const cell = r.children[colIndex];
|
||||||
|
if (cell) values.add(cell.textContent.trim());
|
||||||
|
});
|
||||||
|
const sorted = new Set([...values].sort((a, b) => a.localeCompare(b)));
|
||||||
|
select.innerHTML = '<option value="">-- Alle --</option>' + [...sorted].map(v => `<option value="${v}">${v}</option>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
colSelect.addEventListener('change', () => {
|
||||||
|
state.selectedColumn = colSelect.value;
|
||||||
|
state.searchText = '';
|
||||||
|
state.dropdownValue = '';
|
||||||
|
input.value = '';
|
||||||
|
select.value = '';
|
||||||
|
|
||||||
|
if (!state.selectedColumn) {
|
||||||
|
input.style.display = '';
|
||||||
|
select.style.display = 'none';
|
||||||
|
return applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
const mode = cfg.columnModes[state.selectedColumn];
|
||||||
|
if (mode === 'text') {
|
||||||
|
input.style.display = '';
|
||||||
|
select.style.display = 'none';
|
||||||
|
} else if (mode === 'dropdown') {
|
||||||
|
input.style.display = 'none';
|
||||||
|
select.style.display = '';
|
||||||
|
populateDropdownForColumn(state.selectedColumn);
|
||||||
|
} else {
|
||||||
|
input.style.display = '';
|
||||||
|
select.style.display = 'none';
|
||||||
|
}
|
||||||
|
applyFilters();
|
||||||
|
});
|
||||||
|
input.style.display = '';
|
||||||
|
|
||||||
|
input.addEventListener('input', () => { state.searchText = input.value.toLowerCase(); applyFilters(); });
|
||||||
|
select.addEventListener('change', () => { state.dropdownValue = select.value; applyFilters(); });
|
||||||
|
|
||||||
|
wrap.append(colSelectLabel, colSelect, document.createElement('br'));
|
||||||
|
wrap.append(input, select);
|
||||||
|
container.appendChild(wrap);
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------
|
||||||
|
// Filterbreite an Parent minus Scrollbar
|
||||||
|
//--------------------------------------------
|
||||||
|
function updateFilterWidth() {
|
||||||
|
if (!table || !container) return;
|
||||||
|
|
||||||
|
const wrapper = table.parentElement; // z. B. .table-wrapper
|
||||||
|
if (!wrapper) return;
|
||||||
|
|
||||||
|
const style = getComputedStyle(table.querySelector('thead th'));
|
||||||
|
|
||||||
|
const rect = wrapper.getBoundingClientRect();
|
||||||
|
const scrollbarWidth = wrapper.offsetWidth - wrapper.clientWidth;
|
||||||
|
|
||||||
|
container.style.width = (rect.width - scrollbarWidth - parseFloat(style.paddingRight)) + "px";
|
||||||
|
container.style.maxWidth = (rect.width - scrollbarWidth - parseFloat(style.paddingRight)) + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial setzen
|
||||||
|
updateFilterWidth();
|
||||||
|
|
||||||
|
// Fenstergröße beobachten
|
||||||
|
window.addEventListener('resize', updateFilterWidth);
|
||||||
|
|
||||||
|
// Dynamische Anpassung bei Wrapper Resize
|
||||||
|
const wrapper = table.parentElement;
|
||||||
|
if (wrapper) {
|
||||||
|
new ResizeObserver(updateFilterWidth).observe(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------
|
||||||
|
// Checkbox Filter
|
||||||
|
//------------------------------------
|
||||||
|
function createCheckboxFilter() {
|
||||||
|
const cfgCheck = cfg.checkboxFilter;
|
||||||
|
const idx = getColumnIndex(cfgCheck.column);
|
||||||
|
if (idx < 0) return;
|
||||||
|
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
|
||||||
|
cfgCheck.rules.forEach(rule => {
|
||||||
|
const checkWrapper = document.createElement('label');
|
||||||
|
const checkInput = document.createElement('input');
|
||||||
|
const checkTrack = document.createElement('span');
|
||||||
|
const checkThumb = document.createElement('span');
|
||||||
|
|
||||||
|
checkWrapper.classList.add("cb", "cb-switch");
|
||||||
|
checkWrapper.style.marginRight = '15px'
|
||||||
|
checkInput.type = 'checkbox';
|
||||||
|
checkInput.addEventListener('change', () => {
|
||||||
|
if (checkInput.checked) state.checkbox.add(rule);
|
||||||
|
else state.checkbox.delete(rule);
|
||||||
|
applyFilters();
|
||||||
|
});
|
||||||
|
checkTrack.classList.add('switch-track');
|
||||||
|
checkTrack.setAttribute("aria-hidden", 'true');
|
||||||
|
|
||||||
|
checkThumb.classList.add('switch-thumb');
|
||||||
|
checkThumb.setAttribute("aria-hidden", 'true');
|
||||||
|
|
||||||
|
checkTrack.append(checkThumb)
|
||||||
|
checkWrapper.append(rule.label, checkInput, checkTrack);
|
||||||
|
wrapper.appendChild(checkWrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------
|
||||||
|
// Live Counter
|
||||||
|
//------------------------------------
|
||||||
|
const counter = document.createElement('div');
|
||||||
|
counter.className = 'live-counter';
|
||||||
|
container.appendChild(counter);
|
||||||
|
|
||||||
|
//------------------------------------
|
||||||
|
// Filter Logik
|
||||||
|
//------------------------------------
|
||||||
|
function detectGroups() {
|
||||||
|
if (groupsInitialized) return;
|
||||||
|
groupsInitialized = true;
|
||||||
|
|
||||||
|
const rows = Array.from(table.querySelectorAll('tbody tr'));
|
||||||
|
let currentGroupRow = null;
|
||||||
|
let childBuffer = [];
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
if (row.classList.contains('grouprow')) {
|
||||||
|
// vorige Gruppe speichern
|
||||||
|
if (currentGroupRow && childBuffer.length > 0) {
|
||||||
|
groupMap.set(currentGroupRow, childBuffer);
|
||||||
|
}
|
||||||
|
// neue Gruppe starten
|
||||||
|
currentGroupRow = row;
|
||||||
|
childBuffer = [];
|
||||||
|
}
|
||||||
|
else if (currentGroupRow) {
|
||||||
|
childBuffer.push(row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// letzte Gruppe speichern
|
||||||
|
if (currentGroupRow && childBuffer.length > 0) {
|
||||||
|
groupMap.set(currentGroupRow, childBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// jedem groupRow ein Toggle verpassen (wenn nicht vorhanden)
|
||||||
|
groupMap.forEach((childRows, groupRow) => {
|
||||||
|
const toggle = groupRow.querySelector("span");
|
||||||
|
|
||||||
|
if (!toggle || toggle._groupToggleBound) return;
|
||||||
|
toggle._groupToggleBound = true;
|
||||||
|
|
||||||
|
toggle.addEventListener("click", () => toggleGroup(groupRow));
|
||||||
|
groupRow.addEventListener("dblclick", () => toggleGroup(groupRow));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function toggleGroup(groupRow) {
|
||||||
|
const childRows = groupMap.get(groupRow);
|
||||||
|
if (!childRows) return;
|
||||||
|
|
||||||
|
const toggle = groupRow.querySelector("span");
|
||||||
|
const collapsed = toggle.textContent === '+';
|
||||||
|
|
||||||
|
if (collapsed) {
|
||||||
|
// ausklappen → nur gefilterte Rows zeigen
|
||||||
|
childRows.forEach(r => {
|
||||||
|
if (!r._filteredOut) r.style.display = '';
|
||||||
|
});
|
||||||
|
toggle.textContent = '-';
|
||||||
|
} else {
|
||||||
|
// einklappen → alle verstecken
|
||||||
|
childRows.forEach(r => r.style.display = 'none');
|
||||||
|
toggle.textContent = '+';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function applyFilters() {
|
||||||
|
detectGroups();
|
||||||
|
const rows = Array.from(table.querySelectorAll('tbody tr'));
|
||||||
|
let visible = 0;
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
if (row.classList.contains('grouprow')) { row.style.display = ''; return; }
|
||||||
|
let show = true;
|
||||||
|
|
||||||
|
if (state.selectedColumn === '') {
|
||||||
|
if (state.searchText) show = [...row.cells].some(c => c.textContent.toLowerCase().includes(state.searchText));
|
||||||
|
} else {
|
||||||
|
const idx = getColumnIndex(state.selectedColumn);
|
||||||
|
const mode = cfg.columnModes[state.selectedColumn];
|
||||||
|
if (mode === 'text' && state.searchText) {
|
||||||
|
show = row.cells[idx]?.textContent.toLowerCase().includes(state.searchText);
|
||||||
|
}
|
||||||
|
if (mode === 'dropdown' && state.dropdownValue) {
|
||||||
|
show = row.cells[idx]?.textContent === state.dropdownValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.checkbox.size > 0) {
|
||||||
|
const idx = getColumnIndex(cfg.checkboxFilter.column);
|
||||||
|
const cellVal = row.cells[idx]?.textContent ?? '';
|
||||||
|
const pass = [...state.checkbox].some(rule => rule.test(cellVal));
|
||||||
|
if (!pass) show = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
row._filteredOut = !show; // fürs Gruppensystem speichern
|
||||||
|
|
||||||
|
// Wenn Gruppen existieren:
|
||||||
|
if (groupMap.size > 0) {
|
||||||
|
// Prüfen ob row zu einer Gruppe gehört
|
||||||
|
let parentGroup = null;
|
||||||
|
for (const [g, list] of groupMap.entries()) {
|
||||||
|
if (list.includes(row)) {
|
||||||
|
parentGroup = g;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentGroup) {
|
||||||
|
const toggle = parentGroup.querySelector("span");
|
||||||
|
const collapsed = toggle.textContent === '+';
|
||||||
|
|
||||||
|
if (collapsed) {
|
||||||
|
// eingeklappt → immer ausblenden
|
||||||
|
row.style.display = 'none';
|
||||||
|
if (show) visible++;
|
||||||
|
} else {
|
||||||
|
// ausgeklappt → nur gefilterte anzeigen
|
||||||
|
row.style.display = show ? '' : 'none';
|
||||||
|
if (show) visible++;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (show) visible++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// normale Zeile ohne Gruppe:
|
||||||
|
row.style.display = show ? '' : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
counter.textContent = `${visible} Treffer`;
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------
|
||||||
|
// Init
|
||||||
|
//------------------------------------
|
||||||
|
createDynamicFilterUI();
|
||||||
|
if(options.filterConfig.checkboxFilter) createCheckboxFilter();
|
||||||
|
applyFilters();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (thead) {
|
||||||
|
thead.style.position = 'sticky';
|
||||||
|
thead.style.top = (container.offsetHeight - 1) + 'px';
|
||||||
|
thead.style.zIndex = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------
|
||||||
|
// Public API
|
||||||
|
//------------------------------------
|
||||||
|
return {
|
||||||
|
refreshDropdown() { if (state.selectedColumn && cfg.columnModes[state.selectedColumn] === 'dropdown') {} },
|
||||||
|
reapply() { applyFilters(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
alert(err.stack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//#region Sort table
|
||||||
|
// let sortDirection = {};
|
||||||
|
function sortTable(tableId,colIndex) {
|
||||||
|
const sortDirection = {};
|
||||||
|
const table = document.getElementById(tableId);
|
||||||
|
const tbody = table.tBodies[0];
|
||||||
|
const rows = Array.from(tbody.querySelectorAll("tr"));
|
||||||
|
|
||||||
|
// Toggle sort direction
|
||||||
|
sortDirection[colIndex] = !sortDirection[colIndex];
|
||||||
|
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
const cellA = a.cells[colIndex].textContent.trim();
|
||||||
|
const cellB = b.cells[colIndex].textContent.trim();
|
||||||
|
|
||||||
|
// Numeric sort if possible
|
||||||
|
const numA = parseFloat(cellA);
|
||||||
|
const numB = parseFloat(cellB);
|
||||||
|
if (!isNaN(numA) && !isNaN(numB)) {
|
||||||
|
return sortDirection[colIndex] ? numA - numB : numB - numA;
|
||||||
|
} else {
|
||||||
|
return sortDirection[colIndex]
|
||||||
|
? cellA.localeCompare(cellB)
|
||||||
|
: cellB.localeCompare(cellA);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove old rows
|
||||||
|
tbody.innerHTML = "";
|
||||||
|
rows.forEach(row => tbody.appendChild(row));
|
||||||
|
|
||||||
|
// Update sort arrows
|
||||||
|
const th = table.querySelectorAll("th");
|
||||||
|
th.forEach((header, i) => {
|
||||||
|
header.classList.remove("sort-asc", "sort-desc");
|
||||||
|
if (i === colIndex) {
|
||||||
|
header.classList.add(sortDirection[colIndex] ? "sort-asc" : "sort-desc");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
354
public/javascript/tutorial.js
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
class Tutorial {
|
||||||
|
constructor(steps, options = {}) {
|
||||||
|
this.steps = steps;
|
||||||
|
this.currentStep = 0;
|
||||||
|
this.isRunning = false;
|
||||||
|
this.isWaitingForEvent = false;
|
||||||
|
|
||||||
|
this.options = {
|
||||||
|
overlayOpacity: 0.7,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
this.zIndexBase = 1000;
|
||||||
|
this.zIndexStep = 0;
|
||||||
|
|
||||||
|
this.modifiedElements = new Map();
|
||||||
|
this.baseElement = null;
|
||||||
|
this.currentElement = null;
|
||||||
|
|
||||||
|
this.tooltipObserver = null;
|
||||||
|
this.tooltipRAF = null;
|
||||||
|
this.currentStepElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- UI ----------------
|
||||||
|
|
||||||
|
createUI() {
|
||||||
|
document.addEventListener("keydown", e => {
|
||||||
|
if (this.isWaitingForEvent) return;
|
||||||
|
|
||||||
|
if (e.key === "ArrowRight") this.next();
|
||||||
|
if (e.key === "ArrowLeft") this.prev();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tooltip = document.createElement("div");
|
||||||
|
this.tooltip.id = "tutorial-tooltip";
|
||||||
|
|
||||||
|
Object.assign(this.tooltip.style, {
|
||||||
|
position: "absolute",
|
||||||
|
display: "flex",
|
||||||
|
padding: "12px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
zIndex: 9999,
|
||||||
|
maxWidth: "350px",
|
||||||
|
boxShadow: "0 10px 30px rgba(0,0,0,0.2)",
|
||||||
|
backdropFilter: "blur(5px)",
|
||||||
|
justifyContent: "center"
|
||||||
|
});
|
||||||
|
|
||||||
|
this.text = document.createElement("p");
|
||||||
|
|
||||||
|
this.nextBtn = document.createElement("button");
|
||||||
|
this.nextBtn.innerHTML = ">";
|
||||||
|
this.nextBtn.classList.add("bluebutton");
|
||||||
|
|
||||||
|
this.prevBtn = document.createElement("button");
|
||||||
|
this.prevBtn.innerHTML = "<";
|
||||||
|
this.prevBtn.classList.add("yellowbutton");
|
||||||
|
|
||||||
|
this.nextBtn.onclick = () => this.next();
|
||||||
|
this.prevBtn.onclick = () => this.prev();
|
||||||
|
|
||||||
|
this.tooltip.appendChild(this.text);
|
||||||
|
this.tooltip.appendChild(this.prevBtn);
|
||||||
|
this.tooltip.appendChild(this.nextBtn);
|
||||||
|
|
||||||
|
document.body.appendChild(this.tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Element wait ----------------
|
||||||
|
|
||||||
|
waitForElement(selector, timeout = 5000) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
const check = () => document.querySelector(selector);
|
||||||
|
|
||||||
|
let el = check();
|
||||||
|
if (el) return resolve(el);
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
el = check();
|
||||||
|
if (el) {
|
||||||
|
observer.disconnect();
|
||||||
|
resolve(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
if (Date.now() - start > timeout) {
|
||||||
|
clearInterval(timer);
|
||||||
|
observer.disconnect();
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
el = check();
|
||||||
|
if (el) {
|
||||||
|
clearInterval(timer);
|
||||||
|
observer.disconnect();
|
||||||
|
resolve(el);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Highlight ----------------
|
||||||
|
|
||||||
|
highlightElement(el) {
|
||||||
|
this.removeHighlight();
|
||||||
|
|
||||||
|
if (!this.modifiedElements.has(el)) {
|
||||||
|
this.modifiedElements.set(el, {
|
||||||
|
zIndex: el.style.zIndex,
|
||||||
|
position: el.style.position,
|
||||||
|
boxShadow: el.style.boxShadow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentStep === 0 && !this.baseElement) {
|
||||||
|
this.baseElement = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
let zIndex;
|
||||||
|
|
||||||
|
if (el === this.baseElement) {
|
||||||
|
zIndex = this.zIndexBase;
|
||||||
|
} else {
|
||||||
|
this.zIndexStep++;
|
||||||
|
zIndex = this.zIndexBase + this.zIndexStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.style.position = "relative";
|
||||||
|
el.style.zIndex = zIndex;
|
||||||
|
el.style.boxShadow =
|
||||||
|
"0 0 0 4px rgba(255, 0, 0, 1), 0 0 20px rgba(255,0,0,0.7)";
|
||||||
|
|
||||||
|
this.currentElement = el;
|
||||||
|
|
||||||
|
this.startTooltipTracking(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeHighlight() {
|
||||||
|
if (!this.currentElement) return;
|
||||||
|
|
||||||
|
this.currentElement.style.boxShadow = "";
|
||||||
|
this.stopTooltipTracking();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Event system (NEW) ----------------
|
||||||
|
|
||||||
|
getEventConfig(type) {
|
||||||
|
const map = {
|
||||||
|
click: () => new MouseEvent("click", { bubbles: true, cancelable: true, view: window }),
|
||||||
|
contextmenu: () => new MouseEvent("contextmenu", { bubbles: true, cancelable: true, view: window, button: 2 }),
|
||||||
|
dblclick: () => new MouseEvent("dblclick", { bubbles: true, cancelable: true, view: window }),
|
||||||
|
mouseenter: () => new MouseEvent("mouseenter", { bubbles: true, cancelable: true, view: window }),
|
||||||
|
mouseover: () => new MouseEvent("mouseover", { bubbles: true, cancelable: true, view: window })
|
||||||
|
};
|
||||||
|
|
||||||
|
return map[type] ? map[type]() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeStepOnly(step, el) {
|
||||||
|
if (!step.action) return;
|
||||||
|
|
||||||
|
const event = this.getEventConfig(step.action);
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
el.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForUserEvent(el, type) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const handler = (e) => {
|
||||||
|
el.removeEventListener(type, handler);
|
||||||
|
resolve(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
el.addEventListener(type, handler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Tooltip tracking ----------------
|
||||||
|
|
||||||
|
startTooltipTracking(el) {
|
||||||
|
this.stopTooltipTracking();
|
||||||
|
|
||||||
|
this.currentStepElement = el;
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
if (!this.currentStepElement || !this.tooltip) return;
|
||||||
|
|
||||||
|
const rect = this.currentStepElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.tooltip.style.top = rect.bottom + 10 + "px";
|
||||||
|
this.tooltip.style.left = rect.left + "px";
|
||||||
|
|
||||||
|
this.tooltipRAF = requestAnimationFrame(update);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tooltipObserver = new ResizeObserver(() => {
|
||||||
|
if (!this.currentStepElement) return;
|
||||||
|
|
||||||
|
const rect = this.currentStepElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.tooltip.style.top = rect.bottom + 10 + "px";
|
||||||
|
this.tooltip.style.left = rect.left + "px";
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tooltipObserver.observe(el);
|
||||||
|
|
||||||
|
this.tooltipRAF = requestAnimationFrame(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopTooltipTracking() {
|
||||||
|
if (this.tooltipObserver) {
|
||||||
|
this.tooltipObserver.disconnect();
|
||||||
|
this.tooltipObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.tooltipRAF) {
|
||||||
|
cancelAnimationFrame(this.tooltipRAF);
|
||||||
|
this.tooltipRAF = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentStepElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Steps ----------------
|
||||||
|
|
||||||
|
async showStep() {
|
||||||
|
let step = this.steps[this.currentStep];
|
||||||
|
this.updateButtons(step);
|
||||||
|
|
||||||
|
|
||||||
|
this.isWaitingForEvent = false;
|
||||||
|
|
||||||
|
while (step && (!step.text || step.text.trim() === "")) {
|
||||||
|
const elSilent = await this.waitForElement(step.element);
|
||||||
|
|
||||||
|
if (elSilent) {
|
||||||
|
await this.executeStepOnly(step, elSilent);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentStep++;
|
||||||
|
|
||||||
|
if (this.currentStep >= this.steps.length) {
|
||||||
|
this.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
step = this.steps[this.currentStep];
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = await this.waitForElement(step.element);
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
|
this.next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.highlightElement(el);
|
||||||
|
|
||||||
|
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.tooltip.style.display = "block";
|
||||||
|
this.text.innerHTML = step.text;
|
||||||
|
|
||||||
|
this.tooltip.style.top = rect.bottom + 10 + "px";
|
||||||
|
this.tooltip.style.left = rect.left + "px";
|
||||||
|
|
||||||
|
this.isWaitingForEvent = !!step.waitFor;
|
||||||
|
|
||||||
|
this.nextBtn.style.display = step.waitFor ? "none" : "inline-block";
|
||||||
|
this.prevBtn.style.display =
|
||||||
|
this.currentStep === 0 || step.waitFor ? "none" : "inline-block";
|
||||||
|
|
||||||
|
if (step.waitFor) {
|
||||||
|
await this.waitForUserEvent(el, step.waitFor);
|
||||||
|
this.isWaitingForEvent = false;
|
||||||
|
this.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButtons(step) {
|
||||||
|
const isLastStep = this.currentStep === this.steps.length - 1;
|
||||||
|
|
||||||
|
this.nextBtn.innerHTML = isLastStep ? "Fertig" : ">";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------- Navigation ----------------
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.isRunning) return;
|
||||||
|
|
||||||
|
this.isRunning = true;
|
||||||
|
this.createUI();
|
||||||
|
this.currentStep = 0;
|
||||||
|
this.showStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
next() {
|
||||||
|
if (this.isWaitingForEvent) return;
|
||||||
|
|
||||||
|
this.removeHighlight();
|
||||||
|
|
||||||
|
if (this.currentStep < this.steps.length - 1) {
|
||||||
|
this.currentStep++;
|
||||||
|
this.showStep();
|
||||||
|
} else {
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prev() {
|
||||||
|
if (this.isWaitingForEvent) return;
|
||||||
|
|
||||||
|
this.removeHighlight();
|
||||||
|
|
||||||
|
if (this.currentStep > 0) {
|
||||||
|
this.currentStep--;
|
||||||
|
this.showStep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Cleanup ----------------
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.isRunning = false;
|
||||||
|
|
||||||
|
this.stopTooltipTracking();
|
||||||
|
|
||||||
|
this.modifiedElements.forEach((styles, el) => {
|
||||||
|
el.style.zIndex = styles.zIndex || "";
|
||||||
|
el.style.position = styles.position || "";
|
||||||
|
el.style.boxShadow = styles.boxShadow || "";
|
||||||
|
});
|
||||||
|
|
||||||
|
this.modifiedElements.clear();
|
||||||
|
this.baseElement = null;
|
||||||
|
this.zIndexStep = 0;
|
||||||
|
|
||||||
|
this.tooltip?.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
131
public/styles/colors.css
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
/* #region OS */
|
||||||
|
#desktop { background:var(--theme-desktop-backcolor); background-image:var(--theme-desktop-image); }
|
||||||
|
#taskbar { background:var(--theme-taskbar-backcolor); color:var(--theme-taskbar-color); }
|
||||||
|
#tutorial-tooltip {background:var(--theme-window-backcolor); color:var(--theme-window-color);}
|
||||||
|
#tutorial-tooltip img {filter: invert(1);}
|
||||||
|
|
||||||
|
#start-btn { background:var(--theme-accent-default-backcolor); color:var(--theme-accent-default-color); }
|
||||||
|
#start-btn:hover { background:var(--theme-accent-hover-backcolor); color:var(--theme-accent-hover-color); }
|
||||||
|
#start-btn:active { background:var(--theme-accent-active-backcolor); color:var(--theme-accent-active-color); }
|
||||||
|
|
||||||
|
/* #start-menu, .submenu { background:var(--theme-taskbar-backcolor); color:var(--theme-taskbar-color); } */
|
||||||
|
#start-menu { padding-bottom:10px; background:var(--theme-startmenu-backcolor); color:var(--theme-startmenu-color); border:3px solid rgb(128,128,128); }
|
||||||
|
.start-submenu-head { background:var(--theme-startmenu-submenu-header-backcolor); color:var(--theme-startmenu-submenu-header-backcolor) }
|
||||||
|
.start-icon { background:var(--theme-accent-default-backcolor); }
|
||||||
|
|
||||||
|
/* .start-item:hover, .start-sys-item:hover { color:var(--theme-accent-hover-color); background:var(--theme-accent-hover-backcolor); } */
|
||||||
|
.start-item.has-submenu.open > .menu-label , .start-item:not(.has-submenu.open), .start-sys-item { background-color:var(--theme-startmenu-item-default-backcolor); color:var(--theme-startmenu-item-default-color); }
|
||||||
|
.start-item:not(.has-submenu.open):not(.unload):hover, .start-sys-item:hover { background-color:var(--theme-startmenu-item-hover-backcolor); color:var(--theme-startmenu-item-hover-color); }
|
||||||
|
.start-item:not(.has-submenu):not(.has-submenu.open):not(.unload):active, .start-sys-item:active { color:var(--theme-accent-active-color); background:var(--theme-accent-active-backcolor); }
|
||||||
|
.start-item-sys-container { background:var(--theme-startmenu-syscontainer-backcolor);}
|
||||||
|
.start-item .unload { background:var(--theme-startmenu-item-disabled-backcolor); color:var(--theme-startmenu-item-disabled-color); }
|
||||||
|
/* #taskbar .taskbar-item { background:var(--theme-taskbar-item-backcolor); } */
|
||||||
|
|
||||||
|
.taskbar-item { position: relative;}
|
||||||
|
.taskbar-item::before { background: var(--theme-accent-active-color); }
|
||||||
|
.taskbar-item.focus::before { background: var(--theme-accent-active-backcolor); }
|
||||||
|
/* .taskbar-item.minimized { background:var(--theme-taskbar-item-minimized-backcolor); color:var(--theme-taskbar-item-minimized-color); border-color:var(--theme-taskbar-item-minimized-border-color);} */
|
||||||
|
.taskbar-item.default { background:var(--theme-taskbar-item-default-backcolor); color:var(--theme-taskbar-item-default-color); border-color:var(--theme-taskbar-item-default-border-color);}
|
||||||
|
.taskbar-item:hover { background-color:var(--theme-startmenu-item-hover-backcolor); color:var(--theme-startmenu-item-hover-color); }
|
||||||
|
|
||||||
|
.window { border:2px solid var(--theme-window-titlebar-backcolor); background:var(--theme-window-backcolor); }
|
||||||
|
.window-content { color:var(--theme-window-color); background:var(--theme-window-backcolor); }
|
||||||
|
.window-titlebar { color:var(--theme-window-titlebar-color); background:var(--theme-window-titlebar-backcolor); }
|
||||||
|
.window .controls button { color:var(--theme-window-titlebar-color); background:transparent; }
|
||||||
|
.window .controls button:hover { color:var(--theme-accent-hover-color); background:var(--theme-accent-hover-backcolor); }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Table */
|
||||||
|
table, tr, td, th { border-color:var(--theme-table-border-color); }
|
||||||
|
table thead th { background-color:var(--theme-table-header-backcolor); color:var(--theme-table-header-color); border-color:var(--theme-table-border-color); }
|
||||||
|
table tbody tr:nth-child(even) { background-color:var(--theme-table-rows-even-backcolor); color:var(--theme-table-rows-even-color); }
|
||||||
|
table tbody tr:nth-child(odd) { background-color:var(--theme-table-rows-odd-backcolor); color:var(--theme-table-rows-odd-color); }
|
||||||
|
table tbody tr:not(.grouprow):not(.no-hover):hover { color:var(--theme-accent-hover-color); background:var(--theme-accent-hover-backcolor); }
|
||||||
|
table tbody tr.grouprow { background-color:var(--theme-table-rows-grouprow-backcolor); color:var(--theme-table-rows-grouprow-color); }
|
||||||
|
table tbody tr.active, table tbody td.active { color:var(--theme-accent-active-color); background:var(--theme-accent-active-backcolor); }
|
||||||
|
.table-filter-container { border-color:var(--theme-table-border-color); background-color:var(--theme-table-header-backcolor); color:var(--theme-table-header-color); }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Scrollbar */
|
||||||
|
::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { background:var(--theme-scrollbar-backcolor); }
|
||||||
|
::-webkit-scrollbar-thumb { background-color:var(--theme-scrollbar-thumb-default-backcolor); border-color:var(--theme-scrollbar-thumb-default-border-color); }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background-color:var(--theme-scrollbar-thumb-hover-backcolor); border-color:var(--theme-scrollbar-thumb-hover-border-color); }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Tooltip */
|
||||||
|
.global-tooltip { background:var(--theme-tooltip-backcolor); color:var(--theme-tooltip-color); border-color:var(--theme-tooltip-border-color) }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
/* #region feedbox */
|
||||||
|
.feedbox { background:var(--theme-feedbox-backcolor); color:var(--theme-feedbox-color);box-shadow:0 5px 25px rgba(0,0,0,0.3); }
|
||||||
|
.feedbox-overlay { background:rgba(0,0,0,0.5); }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Messagbox */
|
||||||
|
.test, .test .countdown { color:var(--theme-message-test-color); background-color:var(--theme-message-test-backcolor); border-color:var(--theme-message-test-border-color); }
|
||||||
|
.success, .success .countdown { color:var(--theme-message-success-color); background-color:var(--theme-message-success-backcolor); border-color:var(--theme-message-success-border-color); }
|
||||||
|
.error, .error .countdown { color:var(--theme-message-error-color); background:var(--theme-message-error-backcolor); border-color:var(--theme-message-error-border-color); }
|
||||||
|
.info, .info .countdown { color:var(--theme-message-info-color); background-color:var(--theme-message-info-backcolor); border-color:var(--theme-message-info-border-color); }
|
||||||
|
.warn, .warn .countdown { color:var(--theme-message-warn-color); background-color:var(--theme-message-warn-backcolor); border-color:var(--theme-message-warn-border-color); }
|
||||||
|
.throw_exception, .throw_exception .countdown { color:var(--theme-message-throw-color); background-color:var(--theme-message-throw-backcolor); border-color:var(--theme-message-throw-border-color); }
|
||||||
|
.message { box-shadow:0 4px 10px rgba(0,0,0,0.3); } /* SHADOW? */
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
.error-field { background:var(--theme-message-error-backcolor); border-color:var(--theme-message-error-border-color); color:var(--theme-message-error-color); }
|
||||||
|
.success-field { background:var(--theme-message-success-backcolor); border-color: var(--theme-message-success-border-color); color:var(--theme-message-success-color); }
|
||||||
|
|
||||||
|
/* region Container*/
|
||||||
|
.card { background:var(--theme-container-card-backcolor); border-color:var(--theme-container-card-border); }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Checkbox */
|
||||||
|
.cb-box { border-color:var(--theme-checkbox-default-border-color); box-shadow:0 1px 2px var(--theme-checkbox-shadow-color) inset; }
|
||||||
|
.cb input:checked + .cb-box { background:var(--theme-checkbox-default-thumb); border-color:var(--theme-checkbox-default-thumb); }
|
||||||
|
.cb input:disabled + .cb-box { background:var(--theme-checkbox-disabled-backcolor); border-color:var(--theme-checkbox-disabled-border-color); }
|
||||||
|
.cb-label { color:var(--theme-checkbox-color); }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
/* #region Switch */
|
||||||
|
.cb-switch .cb-label { color:var(--theme-switch-color); }
|
||||||
|
.switch-track { background:var(--theme-switch-backcolor); border-color:var(--theme-switch-border-color); }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
/* #region Required */
|
||||||
|
.is-required-empty { border-width:2px; border-color:var(--theme-required-empty); border-style:solid; }
|
||||||
|
.is-required-filled { border-color:var(--theme-required-accept); }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Tabs */
|
||||||
|
.tabs { border-bottom-color: #ccc; }
|
||||||
|
.tab-content { border-color: #ccc; }
|
||||||
|
.tab:hover { background-color: var(--theme-accent-hover-backcolor); color: var(--theme-accent-hover-color); }
|
||||||
|
.tab.active { background-color: var(--theme-accent-active-backcolor); color: var(--theme-accent-active-color); border-color: var(--theme-accent-active-border-color); border-bottom-color: var(--theme-accent-active-backcolor); }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
/* #region DOM */
|
||||||
|
button:not(:disabled).monolyth { background-color:var(--theme-button-monolyth-default-backcolor); color:var(--theme-button-monolyth-default-color); }
|
||||||
|
button:not(:disabled).monolyth:hover { background-color:var(--theme-button-monolyth-hover-backcolor); color:var(--theme-button-monolyth-hover-color); }
|
||||||
|
button:not(:disabled).monolyth:hover, button:not(:disabled).bluebutton:hover, button:not(:disabled).redbutton:hover, button:not(:disabled).yellowbutton:hover, button:not(:disabled).greenbutton:hover { box-shadow:0 6px 8px rgba(0,0,0,0.15); }
|
||||||
|
|
||||||
|
select { background:var(--theme-select-default-backcolor); color:var(--theme-select-default-color); box-shadow:0 0 5px var(--theme-select-default-border-color); }
|
||||||
|
input:hover, input[type="text"]:hover, input[type="email"]:hover, input[type="password"]:hover, input[type="number"]:hover, input[type="date"]:hover,textarea:hover, select:hover { background:var(--theme-input-hover-backcolor); color:var(--theme-input-hover-color); box-shadow:0 0 5px var(--theme-input-hover-border-color); }
|
||||||
|
input:focus, input[type="text"]:focus, input[type="email"]:focus, input[type="password"]:focus, input[type="number"]:focus, input[type="date"]:focus, textarea:focus, select:focus { background:var(--theme-input-focus-backcolor); color:var(--theme-input-focus-color); box-shadow:0 0 5px var(--theme-input-focus-border-color); }
|
||||||
|
input.valid { border: 2px solid var(--theme-input-accept-border-color); }
|
||||||
|
input.invalid { border: 2px solid var(--theme-input-decline-border-color); }
|
||||||
|
|
||||||
|
*::placeholder, input[id="sAMAccountName"] { color:var(--theme-input-placeholder-color); }
|
||||||
|
|
||||||
|
textarea, input { border-color:var(--theme-input-default-border-color); background:var(--theme-input-default-backcolor); color:var(--theme-input-default-color); }
|
||||||
|
textarea:not([required]):hover, input:not([required]):hover { border-color:var(--theme-input-hover-border-color); background-color:var(--theme-input-hover-backcolor); color:var(--theme-input-hover-color); }
|
||||||
|
textarea:focus, input:focus { border-color:var(--theme-input-focus-border-color); background-color:var(--theme-input-focus-backcolor); color:var(--theme-input-focus-color); }
|
||||||
|
|
||||||
|
select, select option { border-color:var(--theme-select-border-color); background-color:var(--theme-select-backcolor); color:var(--theme-select-color); }
|
||||||
|
select:focus, select:not([required]):hover, select:focus option,select:not([required]):hover option { border-color:var(--theme-select-hover-border-color); background-color:var(--theme-select-hover-backcolor); color:var(--theme-select-hover-color); }
|
||||||
|
|
||||||
|
/* #endregion */
|
||||||
79
public/styles/contextMenu.css
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
.ctx-menu {
|
||||||
|
position: fixed;
|
||||||
|
min-width: 180px;
|
||||||
|
background: var(--theme-contextmenu-backcolor);
|
||||||
|
border: 1px solid var(--theme-contextmenu-border-color);
|
||||||
|
color: var(--theme-contextmenu-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 0;
|
||||||
|
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctx-menu.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctx-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctx-item {
|
||||||
|
padding: 5px 7px 5px 20px;
|
||||||
|
margin-left: 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctx-item:hover {
|
||||||
|
background: var(--theme-contextmenu-hover-backcolor);
|
||||||
|
color: var(--theme-contextmenu-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ctx-item .icon {
|
||||||
|
display: inline-flex;
|
||||||
|
position: relative;
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ctx-disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
/* cursor: default; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctx-divider {
|
||||||
|
border-top: 1px solid var(--theme-contextmenu-divider);
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctx-submenu {
|
||||||
|
position: absolute;
|
||||||
|
left: 100%;
|
||||||
|
top: 0;
|
||||||
|
min-width: 160px;
|
||||||
|
background: var(--theme-contextmenu-backcolor);
|
||||||
|
border: 1px solid var(--theme-contextmenu-border-color);
|
||||||
|
color: var(--theme-contextmenu-color);
|
||||||
|
padding: 4px 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: none;
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctx-submenu.open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctx-arrow {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
434
public/styles/default.css
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
/* #region DOM */
|
||||||
|
*::placeholder { text-align:center; }
|
||||||
|
* {
|
||||||
|
user-select:none;
|
||||||
|
cursor: var(--theme-cursor-default) 1 1, auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.selectable { user-select: text !important; cursor: var(--theme-cursor-pointer) 0 16, pointer; }
|
||||||
|
|
||||||
|
input, input[type="text"], input[type="email"], input[type="password"], input[type="search"], input[type="date"], textarea, select { border-width:2px; border-style:solid; font-size: var(--fontSize); font-family: var(--fontFamily); border-radius:10px; padding:10px 12px; outline:none; transition:all .5s ease; width:auto; }
|
||||||
|
input.width\:100px { width:100px; }
|
||||||
|
input.width\:90px { width:90px; }
|
||||||
|
input.width\:75px { width:75px; }
|
||||||
|
input.width\:50px { width:50px; }
|
||||||
|
input.width\:25px { width:25px; }
|
||||||
|
|
||||||
|
*::placeholder, input[id="sAMAccountName"] { font-style:italic; font-weight:100; letter-spacing:3px; }
|
||||||
|
|
||||||
|
html, button { font-size: var(--fontSize); font-family: var(--fontFamily); }
|
||||||
|
button.monolyth, button.bluebutton, button.greenbutton, button.yellowbutton, button.redbutton { display:inline-block; padding:8px 10px; margin:0.2rem 1.6rem; font-weight:600; text-align:center; text-decoration:none; color:rgb(255, 255, 255); border:none; border-radius:8px; box-shadow:0 4px 6px rgba(0,0,0,0.1); transition:all 0.2s ease; }
|
||||||
|
button.monolyth { background-color:transparent; }
|
||||||
|
button:not(:disabled).monolyth:hover { opacity:0.9; }
|
||||||
|
button.bluebutton { color:var(--theme-button-blue-default-color); background:var(--theme-button-blue-default-backcolor); }
|
||||||
|
button.greenbutton { color:var(--theme-button-green-default-color); background:var(--theme-button-green-default-backcolor); }
|
||||||
|
button.yellowbutton { color:var(--theme-button-yellow-default-color); background:var(--theme-button-yellow-default-backcolor); }
|
||||||
|
button.redbutton { color:var(--theme-button-red-default-color); background:var(--theme-button-red-default-backcolor); }
|
||||||
|
button:disabled { color:var(--theme-button-disabled-color); background:var(--theme-button-disabled-backcolor); }
|
||||||
|
button:not(:disabled).redbutton:hover { background:var(--theme-button-red-hover-color); background:var(--theme-button-red-hover-backcolor); }
|
||||||
|
button:not(:disabled).greenbutton:hover { background:var(--theme-button-green-hover-color); background:var(--theme-button-green-hover-backcolor); }
|
||||||
|
button:not(:disabled).bluebutton:hover { background:var(--theme-button-blue-hover-color); background:var(--theme-button-blue-hover-backcolor); }
|
||||||
|
button:not(:disabled).yellowbutton:hover { background:var(--theme-button-yellow-hover-color); background:var(--theme-button-yellow-hover-backcolor); }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Container */
|
||||||
|
.container.static { width:calc(100% - 20px); margin:10px auto; display:flex; gap:12px; min-height:0; overflow:auto; max-height:100%; flex-direction: column;}
|
||||||
|
/* .card.static { display:flex; flex-direction:column;flex: 0 0 auto; } */
|
||||||
|
.card.static.row { overflow:hidden; display:flex; flex-direction:row; flex-wrap: wrap;}
|
||||||
|
.card.static { overflow:hidden; display:flex; flex-direction:column; }
|
||||||
|
|
||||||
|
.container { width:calc(100% - 20px); margin:10px auto; display:grid; grid-template-columns:100%; gap:12px; min-height:0; overflow:auto; max-height:100%; }
|
||||||
|
.container:not(.static) * { box-sizing:border-box; }
|
||||||
|
.card { border-width:1px; border-style:solid; border-radius:8px; padding:20px; }
|
||||||
|
|
||||||
|
.grid { display:grid; gap:16px; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Copy icon */
|
||||||
|
.copy-icon { transition:opacity .25s; cursor:var(--theme-cursor-pointer) -16 16, pointer; opacity:0.7; }
|
||||||
|
.copy-icon:hover { opacity:1; }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Tooltip */
|
||||||
|
.global-tooltip { position:fixed; padding:6px 8px; border-width:1px; border-style:solid; border-radius:10px; max-width:50vw; white-space:normal; z-index:999; pointer-events:none; opacity:0; transform:translateY(-30px); transition:opacity .12s ease; }
|
||||||
|
.global-tooltip.visible { opacity:1; }
|
||||||
|
/* #endregion*/
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Scrollbar */
|
||||||
|
::-webkit-scrollbar { width:12px; height:12px; }
|
||||||
|
::-webkit-scrollbar-track { border-radius:0px; }
|
||||||
|
::-webkit-scrollbar-thumb { border-radius:6px; border-width:2px; border-style:solid; }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Messagebox */
|
||||||
|
#message-container { position: fixed; top: 1rem; right: 1px; display: flex; flex-direction: column; gap: 0.5rem; z-index: 1000; max-height: 80vh; padding-left: 15px; overflow-y: auto; overflow-x: visible; scrollbar-width: none; -ms-overflow-style: none; }
|
||||||
|
#message-container::-webkit-scrollbar { display: none; }
|
||||||
|
.message { border-radius: 8px; margin: 8px 8px 8px 0; padding: 10px 14px; width: auto; max-width: 45vw; animation: slideIn 0.4s ease-out; transition: transform 0.2s ease; word-break: break-word; overflow-wrap: anywhere; }
|
||||||
|
.message:hover { transform:scale(1.02); }
|
||||||
|
.message-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:6px; }
|
||||||
|
.message-title { flex:1; font-weight:600; }
|
||||||
|
.countdown { margin-right:8px; white-space:nowrap; }
|
||||||
|
.pin-div { margin-left:8px; cursor:var(--theme-cursor-pointer) -16 16, pointer; user-select:none; transition:transform 0.2s ease, color 0.2s ease; }
|
||||||
|
.pin-div:hover { transform:scale(1.2); }
|
||||||
|
.pin-div.pinned { transform:scale(1.1); }
|
||||||
|
@keyframes slideIn { 0% { opacity:0; transform:translateX(100%); } 60% { opacity:1; transform:translateX(-10px); } 80% { opacity:1; transform:translateX(5px); } 100% { opacity:1; transform:translateX(-10px); } }
|
||||||
|
@keyframes slideOut { 0% { opacity:1; transform:translateX(0); } 100% { opacity:1; transform:translateX(100%); } }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Toggle switch */
|
||||||
|
/*
|
||||||
|
<label class="cb cb-switch">
|
||||||
|
<input data-status="{{name}}" type="checkbox" {{#if active}}checked{{/if}} >
|
||||||
|
<span class="switch-track" aria-hidden="true">
|
||||||
|
<span class="switch-thumb" aria-hidden="true"></span>
|
||||||
|
</span>
|
||||||
|
<span class="cb-label">Ein / Aus</span>
|
||||||
|
</label>
|
||||||
|
*/
|
||||||
|
.cb-switch { --w:45px; --h:27px; display:inline-flex; align-items:center; }
|
||||||
|
.cb-switch input { position:absolute; opacity:0; width:0; height:0; pointer-events:none; }
|
||||||
|
.switch-track { width:var(--w); height:var(--h); border-radius:999px; padding:3px; border-width:1px; border-style:solid; box-sizing:border-box; display:inline-flex; align-items:center; transition:background .18s ease, transform .12s ease, border-color .25s ease; }
|
||||||
|
.switch-thumb { min-width:calc(var(--h) - 2 * 3px); width:calc(var(--h) - 2 * 3px); height:calc(var(--h) - 2 * 3px); background:var(--theme-switch-thumb); border-radius:50%; box-shadow:0 2px 6px rgba(0,0,0,.15); transform:translateX(-1px); transition:transform .25s cubic-bezier(.2,.9,.2,1), background .18s ease }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.cb-switch input:disabled + .switch-track { background-color: dimgray;}
|
||||||
|
.cb-switch input:disabled + .switch-track .switch-thumb { background-color: rgb(54, 50, 50); }
|
||||||
|
|
||||||
|
.cb-switch input:not(:disabled):checked + .switch-track { background:var(--theme-switch-active); }
|
||||||
|
|
||||||
|
.cb-switch input:not(:disabled):hover + .switch-track { border-color:var(--theme-switch-hover); }
|
||||||
|
|
||||||
|
.cb-switch input:focus-visible + .switch-track { box-shadow:0 0 0 6px rgba(6,193,103,0.12); }
|
||||||
|
.cb-switch input:checked + .switch-track .switch-thumb { transform:translateX(calc(var(--w) - var(--h))); }
|
||||||
|
.cb-switch label { width: calc(100% - var(--w)); }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region CheckBox */
|
||||||
|
/*
|
||||||
|
<label class="cb cb-modern">
|
||||||
|
<input data-status="{{name}}" type="checkbox" {{#if active}}checked{{/if}} disabled>
|
||||||
|
<span class="cb-box" aria-hidden="true"></span>
|
||||||
|
</label>
|
||||||
|
*/
|
||||||
|
.cb { display:inline-flex; align-items:center; gap:10px; user-select:none; transform:translateY(2px); }
|
||||||
|
.cb input { position:absolute; opacity:0; width:0; height:0; pointer-events:none; }
|
||||||
|
.cb-box { width:20px; height:20px; border-radius:6px; background:var(--theme-checkbox-backcolor); border-width:2px; border-style:solid; display:inline-grid; place-items:center; transition:transform .12s ease, border-color .12s ease, background .12s ease; }
|
||||||
|
.cb-box::after { content:""; width:12px; height:8px; transform:scale(0) translateY(-2px); background-repeat:no-repeat; background-position:center; background-size:contain; background-image:url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 4L4 7l7-7' stroke='%23fff' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); transition:transform .18s cubic-bezier(.2,.9,.2,1); }
|
||||||
|
.cb input:checked + .cb-box { transform:translateY(-1px); }
|
||||||
|
.cb input:checked + .cb-box::after { transform:scale(1) translate(-1px, 1px); }
|
||||||
|
table .cb input:checked + .cb-box::after { transform:scale(1) translate(1px, 1px); }
|
||||||
|
.cb input:focus-visible + .cb-box { outline:none; }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Required */
|
||||||
|
.is-required-empty { border-width:2px; border-style:solid; }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Tabs */
|
||||||
|
.tabs { display: flex; margin-bottom: 10px; border-bottom-width: 2px; border-bottom-style: solid; }
|
||||||
|
.tab { padding: 10px 20px; border: 1px solid transparent; border-top-left-radius: 5px; border-top-right-radius: 5px; margin-right: 5px; transition: background .25s, color .25s, border-color .25s; }
|
||||||
|
.tab-content { border-width: 1px; border-style: solid; border-radius: 5px; padding: 15px; }
|
||||||
|
.item { margin-bottom: 5px; }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Feebox */
|
||||||
|
.feedbox-overlay { position:fixed; top:0; left:0; width:100vw; height:100vh; display:flex; align-items:center; justify-content:center; z-index:999; }
|
||||||
|
.feedbox { border-radius:8px; max-width:50vw; width:100%; padding:20px; animation:feedboxFadeIn 0.2s ease-out; max-height:80vh; display:flex; flex-direction:column; overflow:hidden; }
|
||||||
|
.feedbox h3 { margin:0 0 10px 0; }
|
||||||
|
.feedbox-message { margin-bottom:20px; line-height:1.4; flex:1; overflow-y: auto; /* font-size:1rem; */ }
|
||||||
|
.feedbox-actions { display:flex; justify-content:flex-end; gap:10px; flex-wrap:wrap; flex-shrink:0; }
|
||||||
|
@keyframes feedboxFadeIn { from { transform:translateY(-20px); opacity:0; } to { transform:translateY(0); opacity:1; } }
|
||||||
|
|
||||||
|
.feedbox-btn {
|
||||||
|
padding:6px 14px;
|
||||||
|
/* font-size:0.95rem; */
|
||||||
|
border-radius:4px;
|
||||||
|
border:1px solid #ccc;
|
||||||
|
background:#f5f5f5;
|
||||||
|
transition:all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedbox-btn:hover {
|
||||||
|
background:#e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedbox-btn.primary {
|
||||||
|
background:#4a90e2;
|
||||||
|
color:#fff;
|
||||||
|
border-color:#4a90e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedbox-btn.primary:hover {
|
||||||
|
background:#357ABD;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedbox-btn.danger {
|
||||||
|
background:#e94e3c;
|
||||||
|
color:#fff;
|
||||||
|
border-color:#e94e3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedbox-btn.danger:hover {
|
||||||
|
background:#c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedbox-input {
|
||||||
|
width:100%;
|
||||||
|
padding:6px 8px;
|
||||||
|
/* font-size:0.95rem; */
|
||||||
|
margin-top:8px;
|
||||||
|
margin-bottom:12px;
|
||||||
|
border:1px solid #ccc;
|
||||||
|
border-radius:4px;
|
||||||
|
box-sizing:border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedbox-btn:focus, .feedbox-input:focus {
|
||||||
|
outline:2px solid #4a90e2;
|
||||||
|
outline-offset:2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Small Screens */
|
||||||
|
@media (max-width:400px) {
|
||||||
|
.feedbox {
|
||||||
|
padding:15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedbox-actions {
|
||||||
|
flex-direction:column-reverse;
|
||||||
|
gap:8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedbox-btn {
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.select-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 40px 12px 14px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #333;
|
||||||
|
appearance: none; /* entfernt Standard-Styling */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover / Focus */
|
||||||
|
.select-wrapper select:hover {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4a90e2;
|
||||||
|
box-shadow: 0 0 0 3px rgba(74,144,226,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Pfeil */
|
||||||
|
.select-wrapper::after {
|
||||||
|
content: "▾";
|
||||||
|
position: absolute;
|
||||||
|
right: 14px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.error-field { padding:12px 16px; border-radius:10px; border-width:1px; border-style:solid; }
|
||||||
|
.success-field { border-width: 1px; border-style: solid; padding:12px 16px; border-radius:10px; }
|
||||||
|
|
||||||
|
label { color:var(--muted); display:block; margin-bottom:6px; }
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Dropzone */
|
||||||
|
.dropzone {
|
||||||
|
max-width:360px;
|
||||||
|
padding:12px;
|
||||||
|
box-shadow:0 6px 18px rgba(0,0,0,.08);
|
||||||
|
|
||||||
|
background:var(--theme-dropzone-default-backcolor);
|
||||||
|
|
||||||
|
border:1px dashed #aaa;
|
||||||
|
border-radius:8px;
|
||||||
|
padding:10px;
|
||||||
|
/* font-size:13px; */
|
||||||
|
text-align:center;
|
||||||
|
margin-bottom:8px;
|
||||||
|
color:var(--theme-dropzone-default-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone.active {
|
||||||
|
border-color:var(--theme-dropzone-active-border-color);
|
||||||
|
color:var(--theme-dropzone-active-color);
|
||||||
|
background:var(--theme-dropzone-active-backcolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.dropzone-area ul {
|
||||||
|
list-style:none;
|
||||||
|
padding:0;
|
||||||
|
margin:0;
|
||||||
|
max-height:120px;
|
||||||
|
overflow-y:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone-area li {
|
||||||
|
display:flex;
|
||||||
|
justify-content:space-between;
|
||||||
|
align-items:center;
|
||||||
|
/* font-size:13px; */
|
||||||
|
padding:4px 0;
|
||||||
|
border-bottom:1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.removeButton {
|
||||||
|
border:none;
|
||||||
|
background:none;
|
||||||
|
color:#d11a2a;
|
||||||
|
cursor:var(--theme-cursor-pointer) -16 16, pointer;
|
||||||
|
/* font-size:14px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="file"] {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.drop-hover {
|
||||||
|
outline:3px dashed var(--theme-dropzone-active-border-color);
|
||||||
|
/* outline:2px dashed var(--primary-color, #4a90e2);
|
||||||
|
background:rgba(74, 144, 226, 0.08); */
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.no-drop-hover {
|
||||||
|
outline:3px dashed red;
|
||||||
|
cursor:no-drop !important;
|
||||||
|
/* outline:2px dashed var(--primary-color, #4a90e2);
|
||||||
|
background:rgba(74, 144, 226, 0.08); */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Multiselect textbox */
|
||||||
|
.mst-wrapper {
|
||||||
|
position:relative;
|
||||||
|
display:block;
|
||||||
|
border-radius:4px;
|
||||||
|
padding:4px 8px;
|
||||||
|
background:var(--theme-window-backcolor);
|
||||||
|
|
||||||
|
cursor:text;
|
||||||
|
width:300px; /* optional, je nach gewünschter Breite */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Eingabefeld oben */
|
||||||
|
.mst-input {
|
||||||
|
width:100%;
|
||||||
|
border:none;
|
||||||
|
outline:none;
|
||||||
|
/* font-size:14px; */
|
||||||
|
padding:4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container für Chips unterhalb des Inputs */
|
||||||
|
.mst-chips-container {
|
||||||
|
display:flex;
|
||||||
|
flex-wrap:wrap; /* Chips umbrechen */
|
||||||
|
gap:4px;
|
||||||
|
margin-top:4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Einzelner Chip */
|
||||||
|
.mst-chip {
|
||||||
|
display:inline-flex;
|
||||||
|
align-items:center;
|
||||||
|
background-color:#007bff;
|
||||||
|
color:#fff;
|
||||||
|
border-radius:8px;
|
||||||
|
padding:8px 8px;
|
||||||
|
/* font-size:13px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Entfernen-Button im Chip */
|
||||||
|
.mst-chip-remove {
|
||||||
|
margin-left:4px;
|
||||||
|
cursor:var(--theme-cursor-pointer) -16 16, pointer;
|
||||||
|
font-weight:bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown-Liste */
|
||||||
|
.mst-dropdown {
|
||||||
|
position:absolute;
|
||||||
|
top:100%;
|
||||||
|
left:0;
|
||||||
|
right:0;
|
||||||
|
max-height:200px;
|
||||||
|
overflow-y:auto;
|
||||||
|
border:1px solid #ccc;
|
||||||
|
background:inherit;
|
||||||
|
z-index:1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mst-item {
|
||||||
|
padding:6px 8px;
|
||||||
|
cursor:var(--theme-cursor-pointer) -16 16, pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mst-item:hover {
|
||||||
|
background-color:var(--theme-accent-hover-backcolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mst-item.new {
|
||||||
|
font-style:italic;
|
||||||
|
color:#555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
.tutorial-highlight {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
79
public/styles/jsonTree.css
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
.json-controls {
|
||||||
|
position: sticky;
|
||||||
|
top: -8px;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 2px solid var(--theme-window-titlebar-backcolor);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-controls button {
|
||||||
|
background: var(--theme-window-backcolor) !important;
|
||||||
|
color: var(--theme-window-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-line {
|
||||||
|
padding: 3px 0;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-key {
|
||||||
|
color: #d73a49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-input {
|
||||||
|
padding: 3px 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-input.string {
|
||||||
|
color: #032f62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-input.number {
|
||||||
|
color: #005cc5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-toggle {
|
||||||
|
cursor:var(--theme-cursor-pointer) -16 16, pointer;
|
||||||
|
color: #6f42c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-children {
|
||||||
|
padding: 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-children.collapsed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-add {
|
||||||
|
cursor:var(--theme-cursor-pointer) -16 16, pointer;
|
||||||
|
color: #28a745;
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-add:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-remove {
|
||||||
|
cursor:var(--theme-cursor-pointer) -16 16, pointer;
|
||||||
|
color: #dc3545;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-remove:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
217
public/styles/os.css
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
/* public/css/desktop.css */
|
||||||
|
body, html { margin:0; padding:0; height:100%; overflow: hidden; font-family: var(--fontFamily); font-size: var(--fontSize); }
|
||||||
|
#desktop { position:relative; height:100vh; overflow:hidden; background-size:var(--theme-desktop-background-size); background-repeat: no-repeat; background-position: center; touch-action: none; }
|
||||||
|
|
||||||
|
#windows { z-index: 1; position:absolute; inset:0; padding:8px; box-sizing:border-box; }
|
||||||
|
.window { position:absolute; width: 800px; height: 600px; border-radius:6px; box-shadow: 0 6px 20px rgba(0,0,0,0.4); overflow:hidden; top:50px; left:50px; display:flex; flex-direction:column; resize:both; }
|
||||||
|
.window-titlebar { padding: 0 0 1px 0; height: 28px; display:flex; justify-content:space-between; align-items:center; }
|
||||||
|
.window-icon { height: 20px; background-size:contain; transform: translate(1px, -1px); background-repeat: no-repeat; background-position: left; background-color: rgb(144, 179, 144); padding:4px;border-radius: 8px;}
|
||||||
|
.window-content { display: flex; flex-direction: column; flex:1; padding:8px; overflow: auto; }
|
||||||
|
.window .controls button { transition: background-color .3s, color .3s; padding: 6px 10px; border:none; }
|
||||||
|
.window[class="max"] .window-resize-handle { display: none; }
|
||||||
|
.window-resize-handle { position:absolute; right:0; bottom:0; width:12px; height:12px; cursor:se-resize; z-index: 10; }
|
||||||
|
.window-resize-n { top: -4px; left: 0; right: 0; height: 8px; cursor: n-resize; }
|
||||||
|
.window-resize-s { bottom: -4px; left: 0; right: 0; height: 8px; cursor: s-resize; }
|
||||||
|
.window-resize-e { right: -4px; top: 0; bottom: 0; width: 8px; cursor: e-resize; }
|
||||||
|
.window-resize-w { left: -4px; top: 0; bottom: 0; width: 8px; cursor: w-resize; }
|
||||||
|
.window-resize-ne { top: -4px; right: -4px; width: 12px; height: 12px; cursor: ne-resize; }
|
||||||
|
.window-resize-nw { top: -4px; left: -4px; width: 12px; height: 12px; cursor: nw-resize; }
|
||||||
|
.window-resize-se { bottom: -4px; right: -4px; width: 12px; height: 12px; cursor: se-resize; }
|
||||||
|
.window-resize-sw { bottom: -4px; left: -4px; width: 12px; height: 12px; cursor: sw-resize; }
|
||||||
|
|
||||||
|
|
||||||
|
#taskbar { z-index: 2; position: absolute; width:100%; bottom:0; left:0; height:42px; overflow:visible; display:flex; flex: 0 0 auto; min-width:0; align-items:center; padding:0 8px; box-sizing:border-box; }
|
||||||
|
#start-btn { transition: background-color 0.3s ease; padding: 8px 15px; border-radius: 5px; border: none; margin-right:8px; }
|
||||||
|
#taskbar-windows { display:flex; gap:6px; align-items:center; flex:1; overflow-y:hidden;overflow-x: auto; min-width: 0; }
|
||||||
|
.taskbar-item { display: flex; position: relative; padding:7px 10px; border-radius:4px; }
|
||||||
|
.taskbar-item::before { content: ''; position: absolute; bottom:1px; left:50%; width:40%; height: 4px; border-radius:4px; transform:translateX(-50%) scaleX(0); transform-origin:center; transition:transform 0.3s ease; }
|
||||||
|
.taskbar-item.focus::before { transform: translateX(-50%) scaleX(1); }
|
||||||
|
|
||||||
|
.notify-button { margin-left:auto; flex: 0 0 auto; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.notify-button.resume, .notify-button.pulse { animation: pulse 1.5s infinite; animation-play-state: running; }
|
||||||
|
.notify-button.pause, .notify-button.pause *, .notify-button.active, .notify-button.active * { animation-play-state: paused; }
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.hidden { opacity: 0; pointer-events: none; }
|
||||||
|
|
||||||
|
/* Open submenu vertical */
|
||||||
|
#start-menu { z-index: 2; transition: opacity 0.3s; display:flex; flex-direction:column; position: absolute; bottom: 50px; left: 8px; width: auto; min-width: 300px; border-radius: 8px; overflow: hidden; }
|
||||||
|
.start-header { display: flex; align-items: center; padding: 8px; font-weight: 600; }
|
||||||
|
.start-header > img { height: 24px; width: 24px; margin-right: 5px; }
|
||||||
|
.start-list { list-style: none; margin: 0; padding: 8px 0; height: 60vh; overflow-y: auto; }
|
||||||
|
.start-item { padding: 8px 12px 16px 20px; display: flex; gap: 8px; transition: background 0.2s; border-radius: 4px; }
|
||||||
|
.start-item:not(:has(.submenu)) { padding: 12px;align-items:center; }
|
||||||
|
.start-icon, #start-menu-icon { height: 20px; width: 20px; border-radius: 8px; padding:4px; }
|
||||||
|
.menu-label { display: block; margin-top: 4px; padding-bottom: 0; margin-left: 26px }
|
||||||
|
.start-item.has-submenu { position: relative; display: flex; flex-direction: column; padding-bottom: 8px; }
|
||||||
|
.start-item.has-submenu > .submenu { width: 100%; list-style: none; padding-left: 2px; max-height: 0; overflow: hidden; transition: max-height 0.3s ease; margin: 2px 0 0 8px; }
|
||||||
|
|
||||||
|
.start-item-sys-container { position: relative; left:0; bottom: -8px; padding: 5px 0px; width:100%; display:flex; height:30px; flex-direction:row; justify-content:flex-end; }
|
||||||
|
.start-sys-item { margin: 0 10px 0 8px !important; }
|
||||||
|
|
||||||
|
.start-submenu-head { position: relative; margin-left: 16px; height:32px; display:flex; flex-direction: row; align-items: center; gap:8px; }
|
||||||
|
.start-item.has-submenu > .menu-label::after { content: ""; position: absolute; right: 14px; transform: translateY(50%) rotate(0deg); border: 5px solid transparent; border-top-color: #aaa; transition: transform 0.3s; }
|
||||||
|
.start-item.has-submenu.open > .menu-label::after { content: ""; position: absolute; right: 14px; transform: translateY(50%) rotate(180deg); border: 5px solid transparent; border-top-color: #aaa; transition: transform 0.3s; }
|
||||||
|
.start-item.has-submenu.open > .submenu { max-height: 500px; }
|
||||||
|
.start-item.has-submenu > .submenu li { opacity: 0; transform: translateY(-5px); transition: opacity 0.2s, transform 0.2s; }
|
||||||
|
.start-item.has-submenu.open > .submenu li { opacity: 1; transform: translateY(0); }
|
||||||
|
|
||||||
|
|
||||||
|
img.icon { width: auto; height:20px; object-fit: contain; filter: var(--theme-notifybubble-filter); transform: translate(2px, 2px) }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
|
body, html {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#desktop {
|
||||||
|
height: 100dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fenster = Fullscreen */
|
||||||
|
.window {
|
||||||
|
width: 100% !important;
|
||||||
|
height: calc(100% - 54px) !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
border-radius: 0;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-resize-handle {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-titlebar {
|
||||||
|
height: 29px;
|
||||||
|
padding: 0 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-content {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Taskbar größer für Touch */
|
||||||
|
#taskbar {
|
||||||
|
height: 50px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#start-btn {
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskbar-item {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-list { list-style: none; margin: 0; padding: 8px 0; height: 100%; overflow-y: auto; }
|
||||||
|
|
||||||
|
#start-menu {
|
||||||
|
bottom: 50px !important;
|
||||||
|
height: calc(100dvh - 65px) !important;
|
||||||
|
width: calc(100dvw - 5px);
|
||||||
|
left: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.start-item {
|
||||||
|
padding: 14px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-icon {
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-item.has-submenu > .menu-label { margin: 8px 0 0 40px; }
|
||||||
|
|
||||||
|
.start-item-sys-container { margin:auto; bottom: -10px; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Buttons besser klickbar */
|
||||||
|
.window .controls button {
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notify Button */
|
||||||
|
.notify-button {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) AND (orientation: landscape) {
|
||||||
|
|
||||||
|
body, html {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#desktop {
|
||||||
|
height: 100dvh;
|
||||||
|
}
|
||||||
|
/* Fenster IMMER fullscreen */
|
||||||
|
.window {
|
||||||
|
width: 100% !important;
|
||||||
|
height: calc(100dvh - 46px) !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resize komplett aus */
|
||||||
|
.window-resize-handle {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-titlebar {
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Startmenü bis ganz oben */
|
||||||
|
#start-menu {
|
||||||
|
bottom: 44px !important;
|
||||||
|
height: calc(100dvh - 60px) !important;
|
||||||
|
width: 300px;
|
||||||
|
max-width: 35dvw;
|
||||||
|
left: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Taskbar bleibt sichtbar, aber oben drüber sauber */
|
||||||
|
#taskbar {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Startliste nutzt volle Höhe */
|
||||||
|
.start-list {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-item-sys-container { bottom: -10px;}
|
||||||
|
|
||||||
|
|
||||||
|
.start-item.has-submenu > .menu-label { margin: 8px 0 0 28px; }
|
||||||
|
}
|
||||||
86
public/styles/table.css
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/* .table-wrapper { display:grid; grid-column:auto; overflow:auto; min-height:0; min-width:0;} */
|
||||||
|
.table-wrapper { overflow:auto; min-width:0; }
|
||||||
|
|
||||||
|
.table-wrapper.fit-table { grid-template-columns:calc(1fr - 150px); }
|
||||||
|
/* .table-wrapper { flex:1 1 auto; overflow-y:auto; } */
|
||||||
|
/* .table-wrapper.fit-table { width:100%; max-width:100%; } */
|
||||||
|
/* .table-wrapper.fit-table th.word-wrap, .table-wrapper.fit-table td.word-wrap { white-space:normal; word-wrap:break-word; word-break:break-word; overflow-wrap:anywhere; } */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region Dont's */
|
||||||
|
th.no-wrap, td.no-wrap { white-space:nowrap !important; }
|
||||||
|
th.wrap, td.wrap { white-space:wrap; word-wrap: break-word; word-break: keep-all; }
|
||||||
|
table.no-background tr, table.no-background td { background:none !important; }
|
||||||
|
|
||||||
|
table.border * { border:1px solid white; }
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
|
||||||
|
/* #region performance optimizing */
|
||||||
|
table thead { position:sticky; top:0; z-index:20; will-change:transform; transform:translateZ(0);}
|
||||||
|
|
||||||
|
|
||||||
|
/* #endregion */
|
||||||
|
/* echte Tabelle */
|
||||||
|
table { width:calc(100%); border-spacing:0 5px; }
|
||||||
|
table th, table td { min-width:100px; max-width:250px; overflow:hidden; white-space:nowrap; }
|
||||||
|
|
||||||
|
table tr.grouprow:hover { background: rgba(0,0,0,0.05);}
|
||||||
|
|
||||||
|
/* Header sticky — aber innerhalb echter Tabelle */
|
||||||
|
thead th { position:sticky; top:var(--filter-height); }
|
||||||
|
|
||||||
|
/* KEIN display:block mehr! */
|
||||||
|
thead, tbody { display:table-row-group; }
|
||||||
|
|
||||||
|
|
||||||
|
table thead th { padding:5px; }
|
||||||
|
/* table tbody td { padding:5px 0px 5px 20px; } */
|
||||||
|
table tbody td:not(:first-child):not(:last-child), table thead th:not(:first-child):not(:last-child) { border-width:0; border-style:solid; }
|
||||||
|
table tbody tr.grouprow { font-weight:700; }
|
||||||
|
|
||||||
|
table .text-align\:center { text-align:center; }
|
||||||
|
table .text-align\:right { text-align:right; }
|
||||||
|
table .text-align\:left { text-align:left; }
|
||||||
|
|
||||||
|
td { overflow:hidden; text-overflow:ellipsis; /* verhindert, dass Inhalt die Zelle sprengt */ }
|
||||||
|
|
||||||
|
.table-filter-container {
|
||||||
|
border-bottom-width:8px;
|
||||||
|
border-bottom-style:solid;
|
||||||
|
display:flex;
|
||||||
|
justify-content:flex-start;
|
||||||
|
flex-direction:column;
|
||||||
|
flex-wrap:wrap;
|
||||||
|
gap:10px;
|
||||||
|
|
||||||
|
position:sticky;
|
||||||
|
left:0px;
|
||||||
|
top:0px;
|
||||||
|
/* z-index:20; */
|
||||||
|
padding:5px 10px;
|
||||||
|
border-radius:var(--border-raduis) var(--border-raduis) 0 0;
|
||||||
|
}
|
||||||
|
.table-filter-container .live-counter { position:absolute; right:18px; margin-left:auto; font-weight:bold; }
|
||||||
|
.table-filter-container input, .table-filter-container select { padding:5px !important; }
|
||||||
|
|
||||||
|
|
||||||
|
th.sort-asc::after {
|
||||||
|
content:" ▲";
|
||||||
|
}
|
||||||
|
|
||||||
|
th.sort-desc::after {
|
||||||
|
content:" ▼";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
td.vote { width:28px; height:28px; position:relative; padding:0; }
|
||||||
|
|
||||||
|
td.vote:hover { background:rgba(0,0,0,0.05); /* cursor:pointer;*/ }
|
||||||
|
td.vote::before,
|
||||||
|
td.vote::after { position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); }
|
||||||
|
td.yes::before { content:""; width:8px; height:16px; border:solid #2ecc71; border-width:0 3px 3px 0; transform:translate(-50%, -60%) rotate(45deg); }
|
||||||
|
td.no::before, td.no::after { content:""; width:3px; height:18px; background:#e74c3c; }
|
||||||
|
td.no::before { transform:translate(-50%, -50%) rotate(45deg); }
|
||||||
|
td.no::after { transform:translate(-50%, -50%) rotate(-45deg); }
|
||||||
|
td.maybe::before { content:""; width:0px; height:0px; border-radius:50%; border:10px solid #f1c40f; transform:translate(-50%, -50%); }
|
||||||
67
public/styles/userNotification.css
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/* Bubble-Container */
|
||||||
|
#notify-bubble {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 125%;
|
||||||
|
right: 3px;
|
||||||
|
min-width: 200px;
|
||||||
|
max-height: 50vh;
|
||||||
|
width: max-content; /* 🔑 wächst mit Inhalt */
|
||||||
|
max-width: 600px; /* aber capped */
|
||||||
|
background: var(--theme-taskbar-tray-backcolor);
|
||||||
|
color: var(--theme-taskbar-tray-color);
|
||||||
|
border-color: var(--theme-taskbar-tray-border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow-y: auto; /* vertikal scrollen */
|
||||||
|
overflow-x: hidden; /* horizontal verhindern */
|
||||||
|
box-shadow: 0 8px 20px rgba(0,0,0,0.4);
|
||||||
|
padding: 10px 10px 6px 10px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateY(10px);
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
z-index: 99999999;
|
||||||
|
text-align: end;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bubble-Items als Grid */
|
||||||
|
.bubble-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, auto) 250px;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
/* cursor: pointer; */
|
||||||
|
align-items: center;
|
||||||
|
transition: transform 0.25s;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* linke Spalte */
|
||||||
|
.bubble-item > :nth-child(1) {
|
||||||
|
min-width: 0; /* wichtig für Grid Shrink */
|
||||||
|
white-space: normal; /* Text darf umbrechen */
|
||||||
|
overflow-wrap: normal; /* Wörter nur bei Bedarf umbrechen */
|
||||||
|
word-break: normal; /* nicht mitten in Wörtern */
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* rechte Spalte fix */
|
||||||
|
.bubble-item > :nth-child(2) {
|
||||||
|
width: 250px;
|
||||||
|
height: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hover Effekt */
|
||||||
|
.bubble-item:hover {
|
||||||
|
background: rgba(64,64,64,0.4);
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bubble sichtbar machen */
|
||||||
|
.notify-button.active #notify-bubble {
|
||||||
|
pointer-events: auto;
|
||||||
|
opacity:1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
153
public/views/desktop.hbs
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<head>
|
||||||
|
|
||||||
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
|
<script src="javascript/main.js"></script>
|
||||||
|
<script src="javascript/customModal.js"></script>
|
||||||
|
<script src="javascript/pluginAPI.js"></script>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="styles/default.css">
|
||||||
|
<link rel="stylesheet" href="styles/contextMenu.css">
|
||||||
|
<link rel="stylesheet" href="styles/userNotification.css">
|
||||||
|
<link rel="stylesheet" href="styles/table.css">
|
||||||
|
<link rel="stylesheet" href="styles/jsonTree.css">
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="styles/colors.css" />
|
||||||
|
<link rel="stylesheet" href="styles/os.css" />
|
||||||
|
|
||||||
|
<div id="message-container"></div>
|
||||||
|
<div id="copy-toast"></div>
|
||||||
|
<title>Radix OS</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<div id="desktop">
|
||||||
|
<div id="windows"></div>
|
||||||
|
|
||||||
|
<!-- Startmenu -->
|
||||||
|
<div id="start-menu" class="hidden">
|
||||||
|
<div class="start-header"><img id="start-menu-icon"><span>Radix OS</span></div>
|
||||||
|
<ul class="start-list">
|
||||||
|
{{#groupBy startMenuItems "section"}}
|
||||||
|
|
||||||
|
<li class="start-submenu-head">
|
||||||
|
<span><i>{{this.key}}</i></span>
|
||||||
|
</li>
|
||||||
|
{{#each items}}
|
||||||
|
{{#ifSingle this.menu.items}}
|
||||||
|
{{#if this.authorized}}
|
||||||
|
<li class="start-item {{#unless ../this.active}}unload{{/unless}}" data-active="{{#equaler ../this.active "&&" this.authorized}}true{{else}}false{{/equaler}}" data-appname="{{../this.name}}" data-appview="{{this.view}}" data-viewlabel="{{this.label}}">
|
||||||
|
{{#if this.icon}}
|
||||||
|
<img src="{{#if ../this.pluginPath}}/{{../this.name}}{{/if}}/images/{{this.icon}}" class="start-icon" />
|
||||||
|
{{else}}
|
||||||
|
{{/if}}
|
||||||
|
<span>{{this.label}}</span>
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
<li class="start-item has-submenu">
|
||||||
|
<img src="{{#if ../this.pluginPath}}/{{../this.name}}{{/if}}/images/folder.png" class="start-icon" style="position:absolute;left:12px;"/>
|
||||||
|
<span class="menu-label">{{this.menu.label}}</span>
|
||||||
|
{{!-- {{#if this.version}}<small>v{{this.version}}</small>{{/if}} --}}
|
||||||
|
|
||||||
|
<ul class="submenu">
|
||||||
|
{{#each this.menu.items}}
|
||||||
|
{{#equaler this.label "==" "hr"}}
|
||||||
|
<li><hr></li>
|
||||||
|
{{else}}
|
||||||
|
{{#if this.authorized}}
|
||||||
|
<li class="start-item {{#unless ../this.active}}unload{{/unless}}" data-active="{{#equaler ../this.active "&&" this.authorized}}true{{else}}false{{/equaler}}" data-appname="{{../this.name}}" data-appview="{{this.view}}" data-viewlabel="{{this.label}}">
|
||||||
|
{{#if this.icon}}
|
||||||
|
<img src="{{#if ../this.pluginPath}}/{{../this.name}}{{/if}}/images/{{this.icon}}" class="start-icon" />
|
||||||
|
{{else}}
|
||||||
|
{{/if}}
|
||||||
|
<span>{{this.label}}</span>
|
||||||
|
{{#if this.version}}<small>v{{this.version}}</small>{{/if}}
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
|
{{/equaler}}
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{{/ifSingle}}
|
||||||
|
{{/each}}
|
||||||
|
{{/groupBy}}
|
||||||
|
{{!-- [Function restart] in javascript/main.js --}}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="start-item-sys-container">
|
||||||
|
<button class="monolyth start-sys-item" data-tooltip="Neustart" onclick="restart();" data-tooltip-mode="always">⟳</button>
|
||||||
|
<button class="monolyth start-sys-item" data-tooltip="Abmelden" onclick="logout()" data-tooltip-mode="always">🔓</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Taskbar -->
|
||||||
|
<div id="taskbar">
|
||||||
|
<button id="start-btn">☰</button>
|
||||||
|
<div id="taskbar-windows"></div>
|
||||||
|
<button style="margin-right:0;" class="monolyth notify-button pulse">
|
||||||
|
<img class="icon" src="/images/notifybubble.png">
|
||||||
|
<div id="notify-bubble"></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="javascript/tutorial.js"></script>
|
||||||
|
<script src="javascript/notifyBubble.js"></script>
|
||||||
|
<script src="javascript/contextMenu.js"></script>
|
||||||
|
<script src="javascript/tableFilter.js"></script>
|
||||||
|
<script src="javascript/requiredFields.js"></script>
|
||||||
|
<script src="javascript/loadOnce.js"></script>
|
||||||
|
<script src="javascript/JSON.js"></script>
|
||||||
|
<script src="javascript/os.js"></script>
|
||||||
|
<script>
|
||||||
|
addRestartHook(() => localStorage.setItem('openWindows', JSON.stringify({ })));
|
||||||
|
|
||||||
|
fetch('/api/Plugins/loadScripts', { method: 'POST', headers: {'Content-Type': 'application/json'} })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(scripts => {
|
||||||
|
(scripts).forEach(script => {
|
||||||
|
reloadPluginScript(script);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
loadServerStyles(getCookie('theme'));
|
||||||
|
|
||||||
|
const notifyButtons = document.querySelectorAll('.notify-button');
|
||||||
|
const trayNotifyButton = document.querySelector('#taskbar > .notify-button')
|
||||||
|
const notify = new NotifyBubble(trayNotifyButton, "#notify-bubble");
|
||||||
|
|
||||||
|
document.addEventListener("contextmenu", evt => evt.preventDefault());
|
||||||
|
|
||||||
|
document.querySelectorAll('#start-menu .start-item.has-submenu').forEach(item => {
|
||||||
|
item.addEventListener('click', evt => {
|
||||||
|
// Toggle nur dieses Submenu
|
||||||
|
item.classList.toggle('open');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
const response = await fetch('/logout', { method: 'POST', headers: {'Content-Type': 'application/json'} });
|
||||||
|
if (response.ok) {
|
||||||
|
window.history.pushState({ }, '', '/login');
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/NotifyTray/getTrays')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(trays => {
|
||||||
|
notify.clear();
|
||||||
|
if (trays.length > 0) {
|
||||||
|
trays.forEach(tray => {
|
||||||
|
notify.addItem(tray, () => {
|
||||||
|
if(tray.JSON) {
|
||||||
|
//execution of tray action, e.g. open window
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
127
public/views/eventlog.hbs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Event Log</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="card static">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table id="eventTable" >
|
||||||
|
<thead class="no-wrap">
|
||||||
|
<tr>
|
||||||
|
<th class="text-align:left" onclick="">ID</th>
|
||||||
|
<th class="text-align:left" onclick="">Datum</th>
|
||||||
|
<th class="text-align:left" onclick="">Level</th>
|
||||||
|
<th class="text-align:left" onclick="">Plugin</th>
|
||||||
|
<th class="text-align:left" onclick="">Message</th>
|
||||||
|
<th class="text-align:left" onclick="">Trace</th>
|
||||||
|
<th class="text-align:left" onclick="">User</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each logs}}
|
||||||
|
<tr>
|
||||||
|
<td>{{this.ID}}</td>
|
||||||
|
<td class="no-wrap">{{dateFormat this.Date "yyyy-mm-dd HH:MM:SS"}}</td>
|
||||||
|
<td class="{{this.Level_ID}}">
|
||||||
|
{{LevelDisplayName}}
|
||||||
|
</td>
|
||||||
|
<td>{{this.PluginName}}</td>
|
||||||
|
<td><span>{{replaceAll this.Message "||" "<br>"}}</span> <span class="copy-icon" onclick="copyToClipboard(`{{replaceAll this.Message "||" "<br>"}}`)">⧉</span></td>
|
||||||
|
<td>{{this.Trace}}</td>
|
||||||
|
<td class="no-wrap">{{this.ClearTextUser}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid" style="pointer-events:auto;position:absolute; bottom:18px;right:18px;grid-template-columns: 1fr;">
|
||||||
|
<button class="redbutton" id="confirmClearLog" data-tooltip="Löscht alle Einträge aus der Datenbank">Log leeren</button>
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
const eventTable = document.getElementById("eventTable");
|
||||||
|
let mappedLogs = [];
|
||||||
|
|
||||||
|
|
||||||
|
confirmClearLog.onclick = evt => {
|
||||||
|
feedbox(
|
||||||
|
{ title: `Hast Du Sie nicht mehr Alle?`,
|
||||||
|
message: `<p>Damit werden alle Einträge aus dem EventLog entfernt!</p>`,
|
||||||
|
buttons: {
|
||||||
|
yes: {
|
||||||
|
text: 'Ich weiß, <b>Geht auf meinen Nacken!</b>',
|
||||||
|
onClick: async () => await clearLog()
|
||||||
|
},
|
||||||
|
no: {
|
||||||
|
text: 'Dazu habe ich nicht die Eier'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearLog() {
|
||||||
|
const res = await fetch(`/api/eventlog/clearlog`, { method: 'POST' });
|
||||||
|
const data = await res.json();
|
||||||
|
vt.source([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const vt = virtualTable({
|
||||||
|
tableEl: eventTable,
|
||||||
|
rowHeight: 40,
|
||||||
|
buffer: 5,
|
||||||
|
groupKey: null, // optional zum Gruppieren
|
||||||
|
filterConfig:{
|
||||||
|
exceptedColumns: [''],
|
||||||
|
columnModes:{
|
||||||
|
Plugin: 'dropdown',
|
||||||
|
Level: 'dropdown',
|
||||||
|
User: 'dropdown'
|
||||||
|
},
|
||||||
|
checkboxFilter:{
|
||||||
|
column:'Level_ID',
|
||||||
|
rules:[
|
||||||
|
{ label:'Erfolgreich', test:v => parseInt(v) === 0 },
|
||||||
|
{ label:'Information', test:v => parseInt(v) === 1 },
|
||||||
|
{ label:'Warnung', test:v => parseInt(v) === 2 },
|
||||||
|
{ label:'Fehler', test:v => parseInt(v) === 4 },
|
||||||
|
{ label:'Absturz', test:v => parseInt(v) === 8 },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
customRender: (row, tr) => {
|
||||||
|
tr.style.height = '40px';
|
||||||
|
createTd(tr, row['ID'], { classes: [ 'text-align:left'] });
|
||||||
|
createTd(tr, row['Datum']);
|
||||||
|
createTd(tr, row['Level']);
|
||||||
|
createTd(tr, row['Plugin']);
|
||||||
|
createTd(tr, row['Message'], { classes: [ 'no-wrap' ], attributes: { 'data-tooltip': formatHtml(row['Message']) } });
|
||||||
|
createTd(tr, row['Trace'], { attributes: { 'data-tooltip': formatHtml(row['Trace']) } });
|
||||||
|
createTd(tr, row['User']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
fetch('/api/eventlog/getLogs', { method: 'POST' })
|
||||||
|
.then(logs => logs.json())
|
||||||
|
.then(logs => {
|
||||||
|
mappedLogs = logs.map(row => ({ ...row, Plugin: row['PluginName'], Level: row['LevelDisplayName'], Datum: dateFormat(row['Date'], 'yyyy-mm-dd HH:MM:SS'), User: row['ClearTextUser'] }))
|
||||||
|
vt.addData(mappedLogs.length != 0 ? mappedLogs : {});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
adminSocket.on('eventlog_table', (data) => {
|
||||||
|
data = {...data, Plugin: data['PluginName'], Level: data['LevelDisplayName'], Datum: dateFormat(data['Date'], 'yyyy-mm-dd HH:MM:SS'), User: data['ClearTextUser'] };
|
||||||
|
mappedLogs.unshift(data)
|
||||||
|
vt.source(mappedLogs);
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
36
public/views/help/Hilfe.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
Bilder, Symbole und Zeichnungen wurden durch KI generiert und teilweise aus bereits existierenden, lizenzierten Quellen im Auftrag der Stadt Frankfurt am Main verwendet.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Der Programmcode wurde ebenfalls durch KI unterstützend erstellt. Dies betrifft ausschließlich die Darstellungs- und Designlogik. Alle Verarbeitungen personenbezogener Daten liegen in der Obhut des nutzenden Betriebs.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Alle Inhalte der in RadixOS enthaltenen Plugins dienen ausschließlich dem dienstlichen Betrieb sowie der Weiterentwicklung interner Machine-Learning-Prozesse
|
||||||
|
<i>(automatisiertes Lernen computergestützter Verfahren)</i>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Datenbanken, Inhalte sowie personenbezogene Daten werden nicht an Dritte weitergegeben oder öffentlich zugänglich gemacht. RadixOS ist als geschlossenes System konzipiert.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<b style="color:var(--theme-accent-default-backcolor)">
|
||||||
|
Das Urheberrecht an – RADIX OS – liegt beim Entwickler
|
||||||
|
</b>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
11
public/views/integrated/development.hbs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
test
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
106
public/views/integrated/help.hbs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Hilfe</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
ul#help li a,
|
||||||
|
ul#help li a:visited {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--theme-container-card-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul#help {
|
||||||
|
display:flex;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
left: 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
ul#help li {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container static" style="height: 100vh;">
|
||||||
|
|
||||||
|
<div id="helpTabs" class="tabs" style="overflow-y: auto;scrollbar-width: thin; padding:0;flex: 0 0 auto;"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="card static" style="overflow-y:auto;flex: 1 1 auto;" >
|
||||||
|
<div id="tabWrapper" class="tab-contents" ></div>
|
||||||
|
</div>
|
||||||
|
<div id="helpFooter" class="card static" style="height:auto;bottom:0;text-align:left;flex:0 0 auto;flex-direction:row;justify-content:space-between;gap:4px">
|
||||||
|
<div class="selectable">
|
||||||
|
<div style="color:var(--theme-accent-default-backcolor);font-weight:bold;">
|
||||||
|
<span>© Radix OS</span> <span id="year"></span>
|
||||||
|
<span>Manuel Sowada</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="plugin"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
function showTabs() {
|
||||||
|
fetch(`/api/help/getTabs`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(data => data.json())
|
||||||
|
.then(async data => {
|
||||||
|
data.forEach(async t => {
|
||||||
|
const tabElement = document.createElement('div');
|
||||||
|
tabElement.className = 'tab';
|
||||||
|
tabElement.dataset.tab = t.name;
|
||||||
|
tabElement.textContent = t.name;
|
||||||
|
tabElement.addEventListener('click', async () => {
|
||||||
|
Array.from(document.querySelectorAll('.tab')).forEach(t => t.classList.remove('active'));
|
||||||
|
tabElement.classList.add('active');
|
||||||
|
tabWrapper.innerHTML = '';
|
||||||
|
|
||||||
|
const response = await fetch('/api/help/getHelp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name: t.name })
|
||||||
|
});
|
||||||
|
const item = await response.json();
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<div>
|
||||||
|
<p>${response.status === 500 ? 'Hilfe nicht verfügbar' : item.html}</p>
|
||||||
|
${item.description ? `<hr /><p>${item.description}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const container = document.querySelector('#tabWrapper');
|
||||||
|
container.innerHTML = html;
|
||||||
|
});
|
||||||
|
document.getElementById('helpTabs').appendChild(tabElement);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
showTabs();
|
||||||
|
|
||||||
|
year.textContent = new Date().getFullYear();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
35
public/views/integrated/serverconfig.hbs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<header>
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container static">
|
||||||
|
<div class="card" style="overflow: auto;height:100vh">
|
||||||
|
<div id="jsonConfigTree" class="json-tree"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
fetch('/api/getConfig', { method: 'POST' })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(json => {
|
||||||
|
const tree = createJsonTree({
|
||||||
|
container: document.getElementById("jsonConfigTree"),
|
||||||
|
data: json,
|
||||||
|
expandInitially: true,
|
||||||
|
onSave: json => {
|
||||||
|
fetch('/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(json, null, 2)
|
||||||
|
}).then(() => writeEventLog(0, 'Serverconfig', tree.getChanges()) );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
101
public/views/integrated/serverinfo.hbs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<span style="font-weight: bold;">Dienst</span>
|
||||||
|
<button class="redbutton" id="shutdownButton">Abschalten</button>
|
||||||
|
<button class="yellowbutton" id="restartButton">Neustart</button>
|
||||||
|
<div>
|
||||||
|
<span>PID:</span> <span class="selectable" id="pid"></span>
|
||||||
|
<span class="copy-icon" onclick="copyToClipboard(`${document.getElementById('pid').textContent}`)">⧉</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="table-wrapper" style="max-height: 350px;">
|
||||||
|
<table id="releaseNotes">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-align:left">Erledigt</th>
|
||||||
|
<th class="text-align:left">Timestamp</th>
|
||||||
|
<th class="text-align:left">User</th>
|
||||||
|
<th class="text-align:left">Note</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div id="package" style="overflow:auto"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
shutdownButton.onclick = evt => {
|
||||||
|
fetch('/api/shutdown', { method: 'POST' })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(json => {
|
||||||
|
console.log(json);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
restartButton.onclick = evt => {
|
||||||
|
fetch('/api/restart', { method: 'POST' })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(json => {
|
||||||
|
console.log(json);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('/api/getServerInfo', { method: 'POST' })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(json => {
|
||||||
|
|
||||||
|
document.querySelector('#pid').innerHTML = json.pid;
|
||||||
|
|
||||||
|
|
||||||
|
const vt = virtualTable({
|
||||||
|
tableEl: document.getElementById('releaseNotes'),
|
||||||
|
data: [],
|
||||||
|
buffer: 5,
|
||||||
|
rowHeight: 30,
|
||||||
|
filterConfig:{
|
||||||
|
exceptedColumns: [ 'Erledigt' ],
|
||||||
|
columnModes: {
|
||||||
|
'datetime': 'text',
|
||||||
|
'sAMAccountName': 'text',
|
||||||
|
'Note': 'text'
|
||||||
|
},
|
||||||
|
checkboxFilter: {
|
||||||
|
column: 'finish',
|
||||||
|
rules: [
|
||||||
|
{ label: 'Nur erledigte', test: v => v === true },
|
||||||
|
{ label: 'Nur unerledigte', test: v => v === false },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
customRender: (row, tr) => {
|
||||||
|
createTd(tr,
|
||||||
|
`
|
||||||
|
<label class="cb cb-modern">
|
||||||
|
<input id="id1" data-status="{{name}}" type="checkbox" ${row.finish ? 'checked' : ''} >
|
||||||
|
<span class="cb-box" aria-hidden="true"></span>
|
||||||
|
</label>
|
||||||
|
`, { });
|
||||||
|
createTd(tr, row.datetime, { });
|
||||||
|
createTd(tr, row.sAMAccountName, { });
|
||||||
|
createTd(tr, row.value, { attributes: { "data-tooltip": row.value } });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
vt.addData(json.releaseNotes)
|
||||||
|
document.querySelector('#package').innerHTML = JSON.stringify(json.package).split(',').join('<br>');
|
||||||
|
/*
|
||||||
|
createJsonTree({
|
||||||
|
container: document.getElementById("package-json"),
|
||||||
|
data: json,
|
||||||
|
expandInitially: true,
|
||||||
|
onSave: () => { }
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
</script>
|
||||||
30
public/views/integrated/styleconfig.hbs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<header>
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
<div style="width:100%;height:calc(100% - 64px);">
|
||||||
|
<div id="jsonStyleTree" class="json-tree"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
fetch('/api/getStyles', { method: 'POST' })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(json => {
|
||||||
|
const tree = createJsonTree({
|
||||||
|
container: document.getElementById("jsonStyleTree"),
|
||||||
|
data: json,
|
||||||
|
expandInitially: true,
|
||||||
|
onSave: json => {
|
||||||
|
fetch('/style', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(json, null, 2)
|
||||||
|
}).then(() => writeEventLog(0, 'Stylesheet', JSON.stringify(tree.getChanges())) );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
113
public/views/integrated/usersettings.hbs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<label><b>Farbgebung</b></label>
|
||||||
|
<select id="themeSwitch" style="width: 100%;">
|
||||||
|
<option value="dark">Dunkel</option>
|
||||||
|
<option value="light">Hell</option>
|
||||||
|
<option value="modern">Modern</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<label><b>Schrift</b></label>
|
||||||
|
<select id="fontSelector" style="width: 100%;">
|
||||||
|
<optgroup label="Sans-Serif (Windows Standard)">
|
||||||
|
<option value="Arial">Arial</option>
|
||||||
|
<option value="Calibri">Calibri</option>
|
||||||
|
<option value="Candara">Candara</option>
|
||||||
|
<option value="Segoe UI">Segoe UI</option>
|
||||||
|
<option value="Tahoma">Tahoma</option>
|
||||||
|
<option value="Trebuchet MS">Trebuchet MS</option>
|
||||||
|
<option value="Verdana">Verdana</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
<optgroup label="Serif (Windows Standard)">
|
||||||
|
<option value="Cambria">Cambria</option>
|
||||||
|
<option value="Constantia">Constantia</option>
|
||||||
|
<option value="Georgia">Georgia</option>
|
||||||
|
<option value="Times New Roman">Times New Roman</option>
|
||||||
|
<option value="Palatino Linotype">Palatino Linotype</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
<optgroup label="Monospace / Fixed-Width">
|
||||||
|
<option value="Consolas">Consolas</option>
|
||||||
|
<option value="Courier New">Courier New</option>
|
||||||
|
<option value="Lucida Console">Lucida Console</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
<optgroup label="Fun / Decorative (Windows enthält einige)">
|
||||||
|
<option value="Comic Sans MS">Comic Sans MS</option>
|
||||||
|
<option value="Impact">Impact</option>
|
||||||
|
<option value="Segoe Script">Segoe Script</option>
|
||||||
|
<option value="Segoe Print">Segoe Print</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
</select>
|
||||||
|
<div style="display:flex;flex-direction:row;align-items:center;">
|
||||||
|
<input type="range" id="sizeSlider" min="10" max="32" value="18" style="width:100%;">
|
||||||
|
<span id="sizeValue">18px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="display:flex;flex:1;flex-direction:column;">
|
||||||
|
<label><b>Vollbildmodus</b></label>
|
||||||
|
<button class="bluebutton" id="fullscreenBtn">Einschalten</button>
|
||||||
|
<label>
|
||||||
|
Der Vollbildmodus hat den Vorteil, dass der komplette Bildschirm mit dieser Webseite ausgefüllt wird.<br><i style="color:var(--theme-accent-default-backcolor)">Sehr nützlich für mehr als einen Bildschirme</i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const themeSwitch = document.getElementById("themeSwitch");
|
||||||
|
const selector = document.getElementById("fontSelector");
|
||||||
|
const slider = document.getElementById("sizeSlider");
|
||||||
|
const label = document.getElementById("sizeValue");
|
||||||
|
const fullScreenBtn = document.getElementById("fullscreenBtn");
|
||||||
|
|
||||||
|
fullScreenBtn.addEventListener("click", goFullscreen);
|
||||||
|
function goFullscreen() {
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else {
|
||||||
|
document.documentElement.requestFullscreen();
|
||||||
|
}
|
||||||
|
fullScreenBtn.textContent = !document.fullscreenElement ? "Ausschalten" : "Einschalten";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(savedTheme) { themeSwitch.value = savedTheme; }
|
||||||
|
if(savedFontFamily) { selector.value = savedFontFamily; }
|
||||||
|
if(savedFontSize) { slider.value = savedFontSize; label.textContent = savedFontSize + 'px'; }
|
||||||
|
|
||||||
|
|
||||||
|
themeSwitch.addEventListener('change', (evt) => {
|
||||||
|
const theme = evt.target.value;
|
||||||
|
switchTheme(theme);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function applySettings() {
|
||||||
|
switchTheme(themeSwitch.value);
|
||||||
|
setFontFamily(selector.value);
|
||||||
|
setFontSize(slider.value)
|
||||||
|
}
|
||||||
|
applySettings();
|
||||||
|
|
||||||
|
selector.addEventListener("change", () => {
|
||||||
|
setFontFamily(selector.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
slider.addEventListener("input", () => {
|
||||||
|
setFontSize(parseInt(slider.value))
|
||||||
|
label.textContent = slider.value + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// jede option bekommt ihre eigene Schrift
|
||||||
|
[...selector.options].forEach(option => {
|
||||||
|
option.style.fontFamily = option.value;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
9
public/views/layouts/default.hbs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{{body}}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
187
public/views/login.hbs
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
|
<script src="javascript/main.js"></script>
|
||||||
|
<script src="javascript/customModal.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="styles/default.css">
|
||||||
|
<link rel="stylesheet" href="styles/contextMenu.css">
|
||||||
|
<link rel="stylesheet" href="styles/userNotification.css">
|
||||||
|
<link rel="stylesheet" href="styles/table.css">
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="styles/colors.css" />
|
||||||
|
<link rel="stylesheet" href="styles/os.css" />
|
||||||
|
|
||||||
|
<div id="message-container"></div>
|
||||||
|
<div id="copy-toast"></div>
|
||||||
|
<title>Radix OS</title>
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body id="desktop">
|
||||||
|
{{!-- <form id="login-form" action="/login" method="POST"> --}}
|
||||||
|
<form id="login-form" class="container" style="width:50vw;height:100vh;align-content:center;">
|
||||||
|
<div class="static card" style="flex-direction:column">
|
||||||
|
<label for="sAMAccountName">Benutzername:</label>
|
||||||
|
<input type="text" {{!-- autocomplete="username" --}} id="sAMAccountName" name="sAMAccountName" placeholder="< Vorname.Nachname >" data-tooltip="Melde dich mit deinem Windows-Benutzernamen an.<br>Ist dies deine erste Anmeldung, dann vergib ein neues Kennwort!<br><i style='color:green;'>Hat keine Auswirkungen auf deine Windows-Anmeldung</i>" required>
|
||||||
|
|
||||||
|
<label for="password">Passwort:</label>
|
||||||
|
<input type="password" autocomplete="current-password" id="password" name="password" required>
|
||||||
|
<br>
|
||||||
|
<button class="bluebutton" type="submit">Einloggen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<button class="yellowbutton" id="openSettings" type="button" style="position:absolute;top:10px;right:10px;">Einstellungen</button>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="javascript/notifyBubble.js"></script>
|
||||||
|
<script src="javascript/tableFilter.js"></script>
|
||||||
|
<script src="javascript/requiredFields.js"></script>
|
||||||
|
<script src="javascript/loadOnce.js"></script>
|
||||||
|
<script src="javascript/JSON.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
const sAMAccountName = document.querySelector('#sAMAccountName');
|
||||||
|
const form = document.getElementById('login-form');
|
||||||
|
|
||||||
|
|
||||||
|
loadServerStyles(getCookie('theme'));
|
||||||
|
setCSSVariable('fontFamily', getCookie('fontFamily') ?? 'Verdana');
|
||||||
|
setCSSVariable('fontSize', getCookie('fontSize') ?? '14');
|
||||||
|
setCSSVariable('theme', getCookie('theme') ?? 'light');
|
||||||
|
|
||||||
|
|
||||||
|
form.addEventListener('submit', async e => {
|
||||||
|
try {
|
||||||
|
e.preventDefault(); // verhindert Reload
|
||||||
|
let error = null;
|
||||||
|
const data = new FormData(form);
|
||||||
|
const obj = Object.fromEntries(data.entries());
|
||||||
|
|
||||||
|
// Sende Daten via Fetch an Backend
|
||||||
|
const resSendLoginData = await fetch('/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(obj)
|
||||||
|
});
|
||||||
|
|
||||||
|
if(resSendLoginData.ok) {
|
||||||
|
const loginData = await resSendLoginData.json();
|
||||||
|
|
||||||
|
if(resSendLoginData.status === 200) {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.history.pushState({ }, '', '/');
|
||||||
|
location.reload();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
if(resSendLoginData.status === 401) {
|
||||||
|
|
||||||
|
}
|
||||||
|
showMessage('Login', `${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}<br><br>${loginData.message}`, data.levelId, () => { window.history.pushState({ }, '', '/'); location.reload(); } , 5000);
|
||||||
|
} else {
|
||||||
|
const errorData = await resSendLoginData.json();
|
||||||
|
showMessage('Login', `${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}<br><br>${errorData.message}`, errorData.levelId, () => { window.history.pushState({ }, '', '/'); location.reload(); } , 5000);
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
alert(err)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sAMAccountName.addEventListener('blur', async () => {
|
||||||
|
if(sAMAccountName.value.trim() === '') {
|
||||||
|
sAMAccountName.classList.remove('valid', 'invalid');
|
||||||
|
sAMAccountName.removeAttribute('data-tooltip');
|
||||||
|
sAMAccountName.setAttribute('data-tooltip', "Melde dich mit deinem Windows-Benutzernamen an.<br>Ist dies deine erste Anmeldung, dann vergib ein neues Kennwort!<br><i style='color:green;'>Hat keine Auswirkungen auf deine Windows-Anmeldung</i>")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const responseVerifyAccount = await fetch('/checkLoginName', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ sAMAccountName: sAMAccountName.value })
|
||||||
|
});
|
||||||
|
if(responseVerifyAccount.ok || responseVerifyAccount.status === 200) {
|
||||||
|
sAMAccountName.classList.add('valid');
|
||||||
|
sAMAccountName.setAttribute('data-tooltip', `Du kannst dich mit dem Benutzername <b style="color:green;">${sAMAccountName.value}</b> anmelden`)
|
||||||
|
} else {
|
||||||
|
sAMAccountName.classList.add('invalid');
|
||||||
|
sAMAccountName.setAttribute('data-tooltip', `Der Benutzername <b style="color:red;">${sAMAccountName.value}</b> wurde nicht gefunden`)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('#openSettings').onclick = async evt => {
|
||||||
|
feedbox(
|
||||||
|
{ title: `Einstellungen`,
|
||||||
|
message: /*html*/`
|
||||||
|
<div class="container">
|
||||||
|
<div class="card static">
|
||||||
|
<label>Farbgebung</label>
|
||||||
|
<select id="themeSwitch" onchange="switchTheme(this.value)">
|
||||||
|
<option value="dark" ${getCookie('theme') == 'dark' ? 'selected' : ''}>Dunkel</option>
|
||||||
|
<option value="light" ${getCookie('theme') == 'light' ? 'selected' : ''}>Hell</option>
|
||||||
|
<option value="modern" ${getCookie('theme') == 'modern' ? 'selected' : ''}>Modern</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="card static">
|
||||||
|
<label>Schrift</label>
|
||||||
|
<select id="fontSelector" size="8" onchange="setFontFamily(this.value)">
|
||||||
|
<optgroup label="Sans-Serif (Windows Standard)">
|
||||||
|
<option style="font-family:Arial" value="Arial">Arial</option>
|
||||||
|
<option style="font-family:Calibri" value="Calibri">Calibri</option>
|
||||||
|
<option style="font-family:Candara" value="Candara">Candara</option>
|
||||||
|
<option style="font-family:Segoe UI" value="Segoe UI">Segoe UI</option>
|
||||||
|
<option style="font-family:Tahoma" value="Tahoma">Tahoma</option>
|
||||||
|
<option style="font-family:Trebuchet MS" value="Trebuchet MS">Trebuchet MS</option>
|
||||||
|
<option style="font-family:Verdana" value="Verdana">Verdana</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
<optgroup label="Serif (Windows Standard)">
|
||||||
|
<option style="font-family:Cambria" value="Cambria">Cambria</option>
|
||||||
|
<option style="font-family:Constantia" value="Constantia">Constantia</option>
|
||||||
|
<option style="font-family:Georgia" value="Georgia">Georgia</option>
|
||||||
|
<option style="font-family:Times New Roman" value="Times New Roman">Times New Roman</option>
|
||||||
|
<option style="font-family:Palatino Linotype" value="Palatino Linotype">Palatino Linotype</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
<optgroup label="Monospace / Fixed-Width">
|
||||||
|
<option style="font-family:Consolas" value="Consolas">Consolas</option>
|
||||||
|
<option style="font-family:Courier New" value="Courier New">Courier New</option>
|
||||||
|
<option style="font-family:Lucida Console" value="Lucida Console">Lucida Console</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
<optgroup label="Fun / Decorative (Windows enthält einige)">
|
||||||
|
<option style="font-family:Comic Sans MS" value="Comic Sans MS">Comic Sans MS</option>
|
||||||
|
<option style="font-family:Impact" value="Impact">Impact</option>
|
||||||
|
<option style="font-family:Segoe Script" value="Segoe Script">Segoe Script</option>
|
||||||
|
<option style="font-family:Segoe Print" value="Segoe Print">Segoe Print</option>
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
</select>
|
||||||
|
<div style="display:flex;flex-direction:row;align-items:center">
|
||||||
|
<input type="range" id="sizeSlider" min="10" max="32" value="${savedFontSize ?? '14'}" onchange="setFontSize(parseInt(this.value)); sizeValue.textContent = this.value + 'px';">
|
||||||
|
<span id="sizeValue">${savedFontSize ?? '14'}px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
buttons: {
|
||||||
|
yes: {
|
||||||
|
text: '<b>Fertig</b>',
|
||||||
|
onClick: async () => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
16
public/views/partials/child.hbs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<div class="window" data-winid="{{name}}-{{@index}}" data-plugin="{{name}}">
|
||||||
|
<div class="window-titlebar">
|
||||||
|
<div class="title">{{{viewLabel}}}</div>
|
||||||
|
<div class="controls">
|
||||||
|
{{#if this.printable}}
|
||||||
|
<button data-tooltip="Drucken" id="printbutton">🖨</button>
|
||||||
|
{{/if}}
|
||||||
|
<button class="close">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="window-content">
|
||||||
|
{{{contentHtml}}}
|
||||||
|
</div>
|
||||||
|
<div class="window-resize-handle"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
19
public/views/partials/window.hbs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<div class="window" data-winid="{{appname}}-{{@index}}" data-plugin="{{appname}}">
|
||||||
|
<div class="window-titlebar">
|
||||||
|
<img src="{{#equaler section "==" "Plugin"}}{{appname}}{{/equaler}}/images/{{icon}}" class="window-icon" />
|
||||||
|
<div class="title">{{appname}} [{{label}}]</div>
|
||||||
|
<div class="controls">
|
||||||
|
{{#if tutorial}}
|
||||||
|
<button class="tutorial" id="tutorial-{{appname}}-{{label}}" onclick="tutorial.start()" data-tooltip="Startet eine interaktive Einführung">💡</button>
|
||||||
|
{{/if}}
|
||||||
|
<button class="minimize" style="transform:translateY(0px)">🗕</button>
|
||||||
|
<button class="maximize">▢</button>
|
||||||
|
<button class="close">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="window-content">
|
||||||
|
{{{contentHtml}}}
|
||||||
|
</div>
|
||||||
|
<div class="window-resize-handle"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
242
public/views/plugindashboard.hbs
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Plugin Dashboard</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div id="pluginTabs" class="tabs"></div>
|
||||||
|
<div class="card static">
|
||||||
|
<div class="tab-contents container static" id="pluginsTabWrapper" style="height:100vh;grid-template-columns: repeat(3, 1fr);"></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid" style="pointer-events:auto;position:absolute; bottom:18px;right:18px;grid-template-columns: 1fr;">
|
||||||
|
<button class="bluebutton" id="createNewPlugin">Neues Plugin</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
createNewPlugin.onclick = () => {
|
||||||
|
feedbox({
|
||||||
|
title: 'Neues Plugin erstellen',
|
||||||
|
message: `
|
||||||
|
<input id="newPluginName" placeholder="Name des Plugins" />
|
||||||
|
`,
|
||||||
|
buttons: {
|
||||||
|
yes: {
|
||||||
|
text: '<b>Erstellen</b>',
|
||||||
|
onClick: () => {
|
||||||
|
const name = document.getElementById('newPluginName').value;
|
||||||
|
pluginAPI.create(name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
no: {
|
||||||
|
text: 'Abbrechen',
|
||||||
|
onClick: () => { /* automatisch geschlossen */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
{"name":"Demo",
|
||||||
|
"description":"Beschreibung hier einfügen",
|
||||||
|
"version":"1.0.0.0",
|
||||||
|
"menu":{
|
||||||
|
"label":"Demo",
|
||||||
|
"items":[
|
||||||
|
{"label":"Demo",
|
||||||
|
"view":"index",
|
||||||
|
"defaultSize":{"width":"600px","height":"150px"},
|
||||||
|
"icon":"app.png",
|
||||||
|
"permissions":["*"]}
|
||||||
|
]},
|
||||||
|
"config":{},
|
||||||
|
"active":true
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
fetch('/api/plugins/getAll', { method: 'POST' })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
data.forEach(async metaData => {
|
||||||
|
createTab(pluginTabs, metaData.name, async (tabElement) => {
|
||||||
|
pluginsTabWrapper.innerHTML = '';
|
||||||
|
|
||||||
|
const objectsContainer = document.createElement('div');
|
||||||
|
objectsContainer.className = 'card grid';
|
||||||
|
objectsContainer.style = ``;
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'card static row';
|
||||||
|
card.style = `gap:0 10px;min-height:fit-content;justify-content: center;`;
|
||||||
|
pluginsTabWrapper.appendChild(card);
|
||||||
|
|
||||||
|
const sortedKeys = Object.keys(metaData).sort((a, b) => {
|
||||||
|
if (typeof metaData[a] === 'object' && typeof metaData[b] !== 'object') {
|
||||||
|
return 1;
|
||||||
|
} else if (typeof metaData[a] !== 'object' && typeof metaData[b] === 'object') {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return a.localeCompare(b);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const key of sortedKeys) { // each plugin-metaData
|
||||||
|
if (metaData.hasOwnProperty(key) && metaData[key] !== null && !key.includes('Path')) { // only first level
|
||||||
|
const value = metaData[key];
|
||||||
|
const container = document.createElement('div');
|
||||||
|
|
||||||
|
if(typeof value === 'string') {
|
||||||
|
container.style = `flex:${key === 'description' ? '1 1' : '0 0'} auto;`;
|
||||||
|
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.style = `font-weight:bold`;
|
||||||
|
label.textContent = key.charAt(0).toUpperCase() + key.slice(1);
|
||||||
|
container.appendChild(label);
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.style="width:100%"
|
||||||
|
input.value = value;
|
||||||
|
input.setAttribute('data-name', metaData.name);
|
||||||
|
input.setAttribute('data-field', key);
|
||||||
|
|
||||||
|
new AttachOnBlurChange(input, async (newValue, oldValue) => {
|
||||||
|
if(key === 'name') {
|
||||||
|
await pluginAPI.rename(oldValue, newValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await pluginAPI.update(`${metaData.name}`, { [key]: newValue});
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(input);
|
||||||
|
|
||||||
|
card.appendChild(container);
|
||||||
|
}
|
||||||
|
if(typeof value === 'boolean') {
|
||||||
|
container.style = `flex:0;flex-direction:column; display:flex;align-items:center;`;
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.style = `font-weight:bold`;
|
||||||
|
label.textContent = key.charAt(0).toUpperCase() + key.slice(1);
|
||||||
|
container.appendChild(label);
|
||||||
|
|
||||||
|
const checkbox = document.createElement('label');
|
||||||
|
checkbox.className = 'cb cb-modern';
|
||||||
|
checkbox.innerHTML = `
|
||||||
|
<input onchange="pluginAPI.activation('${metaData.name}', this.checked);" id="plugin-${metaData.name}" data-status="${metaData.name}" type="checkbox" ${metaData.active ? 'checked' : ''}>
|
||||||
|
<span class="cb-box" aria-hidden="true"></span>
|
||||||
|
`;
|
||||||
|
container.appendChild(checkbox);
|
||||||
|
|
||||||
|
card.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof value === 'object') {
|
||||||
|
const objectCard = document.createElement('div');
|
||||||
|
objectCard.className = 'card';
|
||||||
|
objectCard.style = `height:100%`;
|
||||||
|
objectCard.innerHTML = `
|
||||||
|
<label style="font-weight:bold">${key}</label>
|
||||||
|
<div style="overflow:auto" id="${metaData.name}-${key}"></div>
|
||||||
|
`;
|
||||||
|
objectsContainer.appendChild(objectCard);
|
||||||
|
pluginsTabWrapper.appendChild(objectsContainer);
|
||||||
|
const menu = createJsonTree({
|
||||||
|
container: document.getElementById(`${metaData.name}-${key}`),
|
||||||
|
data: metaData[key],
|
||||||
|
schema: {
|
||||||
|
"active": { type: "boolean" }
|
||||||
|
},
|
||||||
|
onChange: (data) => { },
|
||||||
|
onSave: async (data) => {
|
||||||
|
await pluginAPI.update(metaData.name, { [key]: data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pluginsTabWrapper.appendChild(objectsContainer);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*pluginsTabWrapper.innerHTML = `
|
||||||
|
<div class="card static row" style="grid-column: span 3;height:50px;">
|
||||||
|
<label class="cb cb-modern">
|
||||||
|
<label for="plugin-${plugin.name}" style="font-weight:bold">Aktiv</label>
|
||||||
|
<input onchange="pluginAPI.activation('${plugin.name}', this.checked);" id="plugin-${plugin.name}" data-status="${plugin.name}" type="checkbox" ${plugin.active ? 'checked' : ''}>
|
||||||
|
<span class="cb-box" aria-hidden="true"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card static row" style="gap:0 10px;min-height:fit-content;">
|
||||||
|
<div style="flex:0 0 auto">
|
||||||
|
<label style="font-weight:bold">Name</label>
|
||||||
|
<input type="text" data-name="${plugin.name}" data-field="name" value="${plugin.name}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex:0 0 auto">
|
||||||
|
<label style="font-weight:bold">Version</label>
|
||||||
|
<input type="text" data-name="${plugin.name}" data-field="version" value="${plugin.version}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex:1 1 auto">
|
||||||
|
<label style="font-weight:bold">Beschreibung</label>
|
||||||
|
<input type="text" data-name="${plugin.name}" data-field="description" style="width:100%" value="${plugin.description}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card static" style="grid-column: span 3;">
|
||||||
|
<label style="font-weight:bold">Startmenüeinträge</label>
|
||||||
|
<div style="overflow:auto" id="${plugin.name}-menu"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
createJsonTree({
|
||||||
|
container: document.getElementById(`${plugin.name}-menu`),
|
||||||
|
data: plugin,
|
||||||
|
schema: {
|
||||||
|
"active": { type: "boolean" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"name": "TEST",
|
||||||
|
"description": "Remote Desktop Verbindungen über MSRA realisieren",
|
||||||
|
"version": "0.2025.11.07",
|
||||||
|
"config": {},
|
||||||
|
"menu": {
|
||||||
|
"label": "TEST",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"label": "Remoteunterstützung",
|
||||||
|
"view": "index",
|
||||||
|
"icon": "remoteServer.png",
|
||||||
|
"permissions": [
|
||||||
|
"Administration"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
new AttachOnBlurChange(document.querySelector(`[data-name="${plugin.name}"][data-field="name"]`), async (newValue, oldValue) => { await pluginAPI.rename(`${plugin.name}`, newValue); });
|
||||||
|
new AttachOnBlurChange(document.querySelector(`[data-name="${plugin.name}"][data-field="version"]`), async (newValue, oldValue) => await pluginAPI.update(`${plugin.name}`, { version: newValue }))
|
||||||
|
new AttachOnBlurChange(document.querySelector(`[data-name="${plugin.name}"][data-field="description"]`), async (newValue, oldValue) => await pluginAPI.update(`${plugin.name}`, { description: newValue }))
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
253
server.js
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
//#region Modules
|
||||||
|
const { dirname } = require('path');
|
||||||
|
const path = require('path');
|
||||||
|
const https = require('https');
|
||||||
|
var express = require('express');
|
||||||
|
var app = express();
|
||||||
|
var { create } = require('express-handlebars');
|
||||||
|
const cookieParser = require('cookie-parser');
|
||||||
|
var fs = require('fs');
|
||||||
|
var os = require('os');
|
||||||
|
var favicon = require('serve-favicon');
|
||||||
|
const Sequelize = require('sequelize');
|
||||||
|
const { Server } = require('socket.io');
|
||||||
|
const { on } = require('cluster');
|
||||||
|
// const { start } = require('repl');
|
||||||
|
// const WebSocket = require('ws');
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
require('module-alias/register'); // define paths in package.json
|
||||||
|
process.env.TZ = 'Europe/Berlin';
|
||||||
|
|
||||||
|
//#region Paths
|
||||||
|
app.locals.path = {
|
||||||
|
root: dirname(require.main.filename),
|
||||||
|
plugins: `${dirname(require.main.filename)}/plugins`,
|
||||||
|
public: `${dirname(require.main.filename)}/public`,
|
||||||
|
source: `${dirname(require.main.filename)}/src`,
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
//#region Server configuration
|
||||||
|
app.locals.stylesheet = JSON.parse(fs.readFileSync(`${app.locals.path.source}/models/stylesheet.json`, 'utf-8'));
|
||||||
|
app.locals.configuration = JSON.parse(fs.readFileSync(`${app.locals.path.source}/models/configuration.json`, 'utf-8'));
|
||||||
|
app.locals.package = JSON.parse(fs.readFileSync(`${app.locals.path.root}/package.json`, 'utf-8'));
|
||||||
|
|
||||||
|
app.locals.startMenuItems = [ ];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// const server = https.createServer({
|
||||||
|
// key: fs.readFileSync(`${app.locals.path.source}/secure/${app.locals.configuration.certificate.key}`),
|
||||||
|
// cert: fs.readFileSync(`${app.locals.path.source}/secure/${app.locals.configuration.certificate.chain}`),
|
||||||
|
// pfx: fs.readFileSync(`${app.locals.path.source}/secure/${app.locals.configuration.certificate.pfx}`),
|
||||||
|
// passphrase: "password",
|
||||||
|
// //cert: fs.readFileSync(`${app.locals.path.source}/secure/${app.locals.configuration.certificate.chain}`),
|
||||||
|
// }, app);
|
||||||
|
const securePath = `${app.locals.path.source}/secure`;
|
||||||
|
const certConfig = app.locals.configuration.certificate;
|
||||||
|
|
||||||
|
let httpsOptions = {};
|
||||||
|
|
||||||
|
if (certConfig.pfx) {
|
||||||
|
httpsOptions = {
|
||||||
|
pfx: fs.readFileSync(`${securePath}/${certConfig.pfx}`),
|
||||||
|
passphrase: certConfig.passphrase
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
httpsOptions = {
|
||||||
|
key: fs.readFileSync(`${securePath}/${certConfig.key}`),
|
||||||
|
cert: fs.readFileSync(`${securePath}/${certConfig.chain}`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = https.createServer(httpsOptions, app);
|
||||||
|
|
||||||
|
// const wss = new WebSocket.Server({ server });
|
||||||
|
// wss.on('connection', socket => {
|
||||||
|
// socket.send('HELLO')
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
const io = new Server(server, {
|
||||||
|
pingTimeout: 60000,
|
||||||
|
maxHttpBufferSize: 1e8, // 100 MB
|
||||||
|
});
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
//#region Services/DatabaseModel
|
||||||
|
let service = new Map();
|
||||||
|
let databaseModel = new Map();
|
||||||
|
|
||||||
|
let SocketManager = require(`@services/socketManager.js`);
|
||||||
|
let SqlManager = require(`@services/sqlManager.js`);
|
||||||
|
let EventManager = require(`@services/eventManager.js`);
|
||||||
|
let NotifyTrayManager = require(`@services/notifyTrayManager.js`);
|
||||||
|
let PluginManager = require(`@services/pluginManager.js`);
|
||||||
|
let FileSystemManager = require(`@services/fileSystemManager.js`);
|
||||||
|
let AuthenticationManager = require(`@services/authenticationManager.js`);
|
||||||
|
let ActiveDirectory = require(`@services/activeDirectoryManager.js`);
|
||||||
|
|
||||||
|
service.set('socketManager', new SocketManager(io));
|
||||||
|
await service.get('socketManager').addAsync('/');
|
||||||
|
await service.get('socketManager').addAsync('admin');
|
||||||
|
|
||||||
|
service.set('sqlManager', new SqlManager());
|
||||||
|
service.get('sqlManager').addInstance('main', app.locals.configuration.integration.sql.connect);
|
||||||
|
|
||||||
|
databaseModel.set('eventlog', require(`${app.locals.path.source}/models/eventlogModel`)(service.get('sqlManager').getInstance('main')));
|
||||||
|
databaseModel.set('eventlogView', require(`@models/eventlogView`)(service.get('sqlManager').getInstance('main')));
|
||||||
|
service.set('eventManager', new EventManager(app, databaseModel.get('eventlog'), databaseModel.get('eventlogView'), service.get('socketManager')));
|
||||||
|
|
||||||
|
databaseModel.set('notifyTrayModel', require(`@models/notifyTrayModel`)(service.get('sqlManager').getInstance('main')));
|
||||||
|
databaseModel.set('notifyTrayObjectModel', require(`@models/notifyTrayObjectsModel`)(service.get('sqlManager').getInstance('main')));
|
||||||
|
databaseModel.set('notifyTrayView', require(`@models/notifyTrayView`)(service.get('sqlManager').getInstance('main')));
|
||||||
|
service.set('notifyTray', new NotifyTrayManager(databaseModel.get('notifyTrayModel'), databaseModel.get('notifyTrayView'), databaseModel.get('notifyTrayObjectModel')) );
|
||||||
|
|
||||||
|
databaseModel.set('plugin', require(`@models/pluginModel`)(service.get('sqlManager').getInstance('main')));
|
||||||
|
databaseModel.set('authentication', require(`@models/authenticationModel`)(service.get('sqlManager').getInstance('main')));
|
||||||
|
|
||||||
|
service.set('fileSystemManager', new FileSystemManager());
|
||||||
|
service.set('authenticationManager', new AuthenticationManager(databaseModel.get('authentication'), app.locals.configuration.integration.token.secret, service.get('eventManager')));
|
||||||
|
service.set('activeDirectoryManager', new ActiveDirectory(app.locals.configuration.integration.activedirectory))
|
||||||
|
|
||||||
|
// everytime last created service!
|
||||||
|
service.set('pluginManager', new PluginManager(app, databaseModel.get('plugin'), app.locals.path.plugins, app.locals.configuration.plugin.chown, service));
|
||||||
|
|
||||||
|
exports.databaseModel = databaseModel;
|
||||||
|
exports.service = service;
|
||||||
|
exports.path = app.locals.path;
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
//#region Service-Registration/Middleware/Utils/Helpers
|
||||||
|
require(`${app.locals.path.root}/utils.js`);
|
||||||
|
let helpers = service.get('fileSystemManager').loadAllFiles(`${app.locals.path.public}/helpers`, '.js');
|
||||||
|
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(favicon(`${app.locals.path.public}/images/radix_os_icon.ico`));
|
||||||
|
|
||||||
|
app.use(express.static(app.locals.path.root));
|
||||||
|
app.use(express.static(app.locals.path.public));
|
||||||
|
app.use(express.static(app.locals.path.source));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app.use(function(request, response, next) {
|
||||||
|
if (!request.secure) {
|
||||||
|
return response.redirect("https://" + request.headers.host + request.url + app.locals.configuration.server.port);
|
||||||
|
}
|
||||||
|
next(); // Http redirection to secure protocol
|
||||||
|
})
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
//#region App config values
|
||||||
|
app.set('view engine', '.hbs');
|
||||||
|
app.set('views', [
|
||||||
|
`${app.locals.path.public}/views`,
|
||||||
|
`${app.locals.path.public}/views/integrated`
|
||||||
|
]);
|
||||||
|
app.set('trust proxy', true)
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
//#region Error exception handling
|
||||||
|
app.on('uncaughtException', (err) => service.get('eventManager').write(null, 8, null, err ));
|
||||||
|
process.on('uncaughtException', (err) => service.get('eventManager').write(null, 8, null, err ));
|
||||||
|
process.on('unhandledRejection', (reason, promise) => service.get('eventManager').write(null, 8, null, reason ));
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
app.engine('hbs', create({
|
||||||
|
extname: 'hbs',
|
||||||
|
helpers: helpers,
|
||||||
|
partialsDir: `${app.locals.path.public}/views/partials`,
|
||||||
|
layoutsDir: `${app.locals.path.public}/views/layouts`,
|
||||||
|
defaultLayout: `${app.locals.path.public}/views/layouts/default.hbs`
|
||||||
|
}).engine)
|
||||||
|
|
||||||
|
|
||||||
|
server.listen(app.locals.configuration.server.port, () => {
|
||||||
|
(async () => {
|
||||||
|
const databaseTest = await service.get('sqlManager').test("main"); // Check if database connection is established
|
||||||
|
service.get('eventManager').write(null, databaseTest.levelId, null, databaseTest.message);
|
||||||
|
|
||||||
|
// Loading plugins
|
||||||
|
const plugins = await service.get('pluginManager').loadAll()
|
||||||
|
// const pluginsLoaded = {
|
||||||
|
// levelId: plugins.some(plugin => plugin.levelId > 0) ? 2 : 0,
|
||||||
|
// message: plugins.map(plugin => `${plugin.pluginName} v${plugin.metadata.version} ${plugin.message}`).join('<br>')
|
||||||
|
// }
|
||||||
|
// service.get('eventManager').write(null, pluginsLoaded.levelId, null, pluginsLoaded.message);
|
||||||
|
|
||||||
|
plugins.forEach(plugin => {
|
||||||
|
service.get('eventManager').write(null, plugin.levelId, null, `${plugin.pluginName} v${plugin.metadata.version} ${plugin.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
//#region Menu-Generator
|
||||||
|
app.use(async (req, res, next) => {
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
//#region Implement routes
|
||||||
|
require(`${app.locals.path.source}/routes/indexRoutes.js`).route(app, service);
|
||||||
|
require(`${app.locals.path.source}/routes/loginRoutes.js`).route(
|
||||||
|
app,
|
||||||
|
service.get('authenticationManager'),
|
||||||
|
service.get('socketManager'),
|
||||||
|
service.get('eventManager')
|
||||||
|
);
|
||||||
|
require(`${app.locals.path.source}/routes/adminRoutes.js`).route(
|
||||||
|
app,
|
||||||
|
service.get('authenticationManager'),
|
||||||
|
service.get('pluginManager'),
|
||||||
|
service.get('eventManager'),
|
||||||
|
service.get('socketManager'),
|
||||||
|
service.get('activeDirectoryManager'),
|
||||||
|
`${app.locals.path.source}/models/stylesheet.json`
|
||||||
|
);
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
//#region Implements sockets
|
||||||
|
require(`${app.locals.path.source}/sockets/mainSocket.js`)(
|
||||||
|
app,
|
||||||
|
service.get('socketManager'),
|
||||||
|
'/',
|
||||||
|
service.get('pluginManager'),
|
||||||
|
databaseModel.get('authentication'),
|
||||||
|
service.get('fileSystemManager'),
|
||||||
|
service.get('eventManager'),
|
||||||
|
service.get('activeDirectoryManager')
|
||||||
|
);
|
||||||
|
require(`${app.locals.path.source}/sockets/adminSocket.js`)(
|
||||||
|
app,
|
||||||
|
service.get('socketManager'),
|
||||||
|
'admin',
|
||||||
|
service.get('eventManager')
|
||||||
|
);
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
service.get('eventManager').write(null, 1, null,
|
||||||
|
`${app.locals.configuration.server.name} is running`,
|
||||||
|
`fqdn: https://${os.hostname()}:${app.locals.configuration.server.port}/`,
|
||||||
|
`process id: ${process.pid}`,
|
||||||
|
`url: ${os.hostname()}`,
|
||||||
|
`port: ${app.locals.configuration.server.port}`
|
||||||
|
)
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
})();
|
||||||
138
skeleton.txt
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
howto:
|
||||||
|
--> Watch out for names. Stylesheet and javascript names, will be used global!
|
||||||
|
-----------------------------------------------------------------------------------------------------------------------
|
||||||
|
├─ create new plugin:
|
||||||
|
│ ├─ add line to index.js: 'service.get('socketManager').add("%namespace%");'
|
||||||
|
│ ├─ add context in plugins route.js:
|
||||||
|
│ │ └─ app.post('/window/:name/index', async (req, res) => { await renderWindow(app, 'index', metadata, { user: await vUser.findOne({ where: { Benutzer: req.cookies.sAMAccountName }, raw: true } ) }, res) });
|
||||||
|
│ └─
|
||||||
|
├─ create new socket namespaces:
|
||||||
|
│ └─ service.get('socketManager').add("%namespace%");
|
||||||
|
└─ add new startmenu entries:
|
||||||
|
├─ add context object to file '/src/models/integratedStartmenuItems.js', where you can define pass through context paramters
|
||||||
|
└─ add view-template to '/public/views/integrated/'
|
||||||
|
-----------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
useful global features:
|
||||||
|
-----------------------------------------------------------------------------------------------------------------------
|
||||||
|
├─ %plugin%/public/javascripts.main.js ** for each plugin, main.js is loading for global use
|
||||||
|
├─ eventlog
|
||||||
|
│ ├─ service.get('eventManager).write(...) ** writes log entry and notifys admins on webui
|
||||||
|
│ └─ service.get('eventManager).writeLog(...) ** writes log entry only without firing webui message
|
||||||
|
├─ notifyTray
|
||||||
|
│ └─ service.get('notifyTray').createAndNotify ...
|
||||||
|
└─
|
||||||
|
-----------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
rules:
|
||||||
|
-----------------------------------------------------------------------------------------------------------------------
|
||||||
|
├─ filesystem:
|
||||||
|
│ ├─ app descriptions, file/folder names in English; except proper names
|
||||||
|
│ ├─ files and folders always lowercase; continue in uppercase instead of spaces
|
||||||
|
│ ├─ files in routes folder, have to be named %name%Routes.js to generate dynamic menu
|
||||||
|
│ ├─ folder names in plural, if files are included
|
||||||
|
│ └─ folder names in singular, if files are excluded
|
||||||
|
├─ code:
|
||||||
|
| ├─ javascript:
|
||||||
|
| │ ├─ ids, classes, data-attributes (names and values) with hyphen (Bindestrich) instead of spaces; always lowercase
|
||||||
|
| │ └─ functions (and parameters in it) always lowercase; continue in uppercase instead of spaces
|
||||||
|
| ├─ socket.io:
|
||||||
|
| │ └─ socket names with underscores instead of spaces; always lowercase
|
||||||
|
| ├─ json:
|
||||||
|
| │ └─ variables in strings: "${}" (e.g ${ROOTPATH})
|
||||||
|
└─ others:
|
||||||
|
├─ admin eventlog in English
|
||||||
|
├─ regions in English; first letter and proper names in capital
|
||||||
|
└─ users eventlog/messaging in German
|
||||||
|
-----------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
folder structur:
|
||||||
|
-----------------------------------------------------------------------------------------------------------------------
|
||||||
|
root/ ** server relevant files
|
||||||
|
│
|
||||||
|
├─ node_modules/ ** npm third party modules for nodejs server
|
||||||
|
├─ plugins/ ** server webapp based extensions
|
||||||
|
│ └─ %plugin_name%/ ** dynamic name reservation
|
||||||
|
│ ├─ views/ ** contains handlebar views
|
||||||
|
│ ├─ public/ ** contains plugin specified static files
|
||||||
|
│ │ ├─ helpers/ ** contains plugin specified handlebars helpers function
|
||||||
|
│ │ ├─ javascript/ ** contains plugin specified javascript code
|
||||||
|
│ │ ├─ others/ ** contains plugin specified files like options or templates
|
||||||
|
│ │ └─ styles/ ** contains plugin specified stylesheets
|
||||||
|
│ ├─ docs/
|
||||||
|
│ │ ├─ help.hbs *** helper file for webapp description
|
||||||
|
│ │ └─ tutorial.hbs *** tutorial file, for webapp tutorial
|
||||||
|
│ ├─ index.js *** plugin entry point
|
||||||
|
│ ├─ plugin.json *** metadata basefile
|
||||||
|
│ └─ sockets.js *** plugin specified sockets
|
||||||
|
├─ public/ ** contains all visible static files
|
||||||
|
│ ├─ helpers/ ** javascript express handlebars helpers for client views
|
||||||
|
│ ├─ images/ ** global images
|
||||||
|
│ ├─ styles/ ** global stylesheets file
|
||||||
|
│ │ ├─ responsive/
|
||||||
|
│ │ │ ├─ desktop.css *** responsive layout for desktops
|
||||||
|
│ │ │ ├─ mobile.css *** responsive layout for smartphones
|
||||||
|
│ │ │ └─ tablet.css *** responsive layout for tablets
|
||||||
|
│ ├─ dark.css *** dark theme
|
||||||
|
│ ├─ light.css *** light theme
|
||||||
|
│ └─ default.css *** global layout for everything everywhere all at once
|
||||||
|
│ └─ views/ ** contains the handlebars views
|
||||||
|
│ ├─ layouts/ ** templates for views
|
||||||
|
│ │ ├─ default.hbs *** main layout for everything everywhere all at once
|
||||||
|
│ │ └─ stricted_area.hbs *** layout without menus and links
|
||||||
|
│ └─ partials/ ** view snippets for multiple use
|
||||||
|
├─ src/ ** files for handle server inquiries
|
||||||
|
│ ├─ controllers/ ** contains the business logic receiving and validating client inquiries
|
||||||
|
│ ├─ models/ ** data/-base models
|
||||||
|
│ │ ├─ configuration.json *** server configuration file
|
||||||
|
│ │ └─ stylesheets.json *** global client stylesheet variabels
|
||||||
|
│ ├─ routes/ ** handles endpoint routing
|
||||||
|
│ ├─ secure/ ** contains encrypted passfiles and certificates
|
||||||
|
│ ├─ services/ ** contains business logic classes
|
||||||
|
│ │ ├─ authentication.js *** authenticates client connection attempts
|
||||||
|
│ │ ├─ encryption.js *** de-/ encryption serialization class
|
||||||
|
│ │ ├─ eventlog.js *** messaging class; client and server based
|
||||||
|
│ │ ├─ pluginsystem.js *** plugin engine
|
||||||
|
│ │ └─ sql.js *** microsoft sql connection and processing queries
|
||||||
|
│ └─ sockets/ ** global sockets
|
||||||
|
│ development.json *** development to do's
|
||||||
|
│ license_internal.txt *** license text file
|
||||||
|
│ package-lock.json *** npm link to node_modules
|
||||||
|
│ package.json *** file for server initialization
|
||||||
|
│ release_notes.json *** shows server version release notes
|
||||||
|
└─ server.js *** file for handle server start
|
||||||
|
-----------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
stucture of style.json:
|
||||||
|
-----------------------------------------------------------------------------------------------------------------------
|
||||||
|
root
|
||||||
|
│
|
||||||
|
├─ themes:
|
||||||
|
│ ├─ dark:
|
||||||
|
│ └─ light:
|
||||||
|
└─ responsive:
|
||||||
|
├─ desktop:
|
||||||
|
├─ tablet:
|
||||||
|
└─ mobile:
|
||||||
|
-----------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
structure of plugin:
|
||||||
|
-----------------------------------------------------------------------------------------------------------------------
|
||||||
|
root/
|
||||||
|
│
|
||||||
|
├─ plugins/
|
||||||
|
│ ├─ ${plugin}/
|
||||||
|
│ │ ├─ views/
|
||||||
|
│ │ │ └─ index.hbs (main file)
|
||||||
|
│ │ ├─ styles/
|
||||||
|
│ │ │ └─ *.css (Set stylesheet as usual in header-section)
|
||||||
|
│ │ ├─ scripts/
|
||||||
|
│ │ │ └─ *.js (Load file with function reloadPluginScript("/${pluginname}/javascript/${name}.js") in script-section)
|
||||||
|
│ │ └─ files/
|
||||||
|
│ │ └─ *.png (Set the name exactly as it appears in the plugin.json file)
|
||||||
|
│ └─ plugin.json
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------------------------------------------------
|
||||||
84
src/models/authenticationModel.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const Authentication = sequelize.define('Authentication', {
|
||||||
|
ObjectGUID: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
sAMAccountName: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
mail: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
givenName: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
sn: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
employeeID: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
department: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
streetAddress: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
userAccountControl_ID: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
authenticationType_ID: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
telephoneNumber: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
physicalDeliveryOfficeName: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
distinguishedName: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true, // Passwort wird erst bei Erstanmeldung gesetzt
|
||||||
|
},
|
||||||
|
refreshtoken: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
online: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
tableName: 'Authentication', // Tabellenname in der Datenbank
|
||||||
|
timestamps: false, // Falls du keine createdAt/updatedAt Spalten hast
|
||||||
|
})
|
||||||
|
return Authentication;
|
||||||
|
};
|
||||||
51
src/models/eventlogModel.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
next(`database connection has been established successfully`);
|
||||||
|
} catch(error) {
|
||||||
|
next([`unable to connect to the database`, error]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
const EventLog = sequelize.define('EventLog', {
|
||||||
|
ID: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true, // sorgt für Auto-Inkrement
|
||||||
|
},
|
||||||
|
Message: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
Trace: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
Level_ID: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
PluginName: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
Date: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
ObjectGUID: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
tableName: 'EventLog', // Tabellenname in der Datenbank
|
||||||
|
timestamps: false, // Falls du keine createdAt/updatedAt Spalten hast
|
||||||
|
})
|
||||||
|
|
||||||
|
return EventLog;
|
||||||
|
};
|
||||||
34
src/models/eventlogView.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const EventLogView = sequelize.define('EventLogView', {
|
||||||
|
ID: { type: DataTypes.INTEGER, primaryKey: true },
|
||||||
|
Message: { type: DataTypes.STRING },
|
||||||
|
Trace: { type: DataTypes.STRING },
|
||||||
|
Surname: { type: DataTypes.STRING },
|
||||||
|
givenName: { type: DataTypes.STRING },
|
||||||
|
Date: { type: DataTypes.DATE },
|
||||||
|
department: { type: DataTypes.STRING },
|
||||||
|
ClearTextUser: { type: DataTypes.STRING },
|
||||||
|
ObjectTypDisplayName: { type: DataTypes.STRING },
|
||||||
|
Level_ID: { type: DataTypes.INTEGER },
|
||||||
|
LevelName: { type: DataTypes.STRING },
|
||||||
|
LevelPriority: { type: DataTypes.INTEGER },
|
||||||
|
LevelDisplayName: { type: DataTypes.STRING },
|
||||||
|
PluginName: { type: DataTypes.STRING },
|
||||||
|
ObjectGUID: { type: DataTypes.UUID },
|
||||||
|
sAMAccountName: { type: DataTypes.STRING },
|
||||||
|
mail: { type: DataTypes.STRING },
|
||||||
|
Phone: { type: DataTypes.STRING },
|
||||||
|
Office: { type: DataTypes.STRING },
|
||||||
|
Adress: { type: DataTypes.STRING },
|
||||||
|
authenticationType_ID: { type: DataTypes.INTEGER },
|
||||||
|
TypeName: { type: DataTypes.STRING }
|
||||||
|
}, {
|
||||||
|
tableName: 'vEventLog', // dein SQL-View
|
||||||
|
timestamps: false,
|
||||||
|
freezeTableName: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return EventLogView;
|
||||||
|
};
|
||||||
108
src/models/integratedStartmenuItems.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
module.exports = ([
|
||||||
|
{
|
||||||
|
section: "System",
|
||||||
|
name: 'Server',
|
||||||
|
active: true,
|
||||||
|
menu: {
|
||||||
|
label: 'Server',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Styles',
|
||||||
|
view: 'styleconfig',
|
||||||
|
icon: 'brush.png',
|
||||||
|
permissions: [ 'Administration' ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Configs',
|
||||||
|
view: 'serverconfig',
|
||||||
|
icon: "app.png",
|
||||||
|
permissions: [ 'Administration' ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: 'System',
|
||||||
|
name: 'EventLog',
|
||||||
|
active: true,
|
||||||
|
menu: {
|
||||||
|
label: 'EventLog',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'EventLog',
|
||||||
|
view: 'eventlog',
|
||||||
|
defaultSize: { width: "1200px", height: "1200px" },
|
||||||
|
icon: "eventlog.ico",
|
||||||
|
permissions: [ 'Administration' ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: 'System',
|
||||||
|
name: 'Plugins',
|
||||||
|
active: true,
|
||||||
|
menu: {
|
||||||
|
label: 'Plugins',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Plugins',
|
||||||
|
view: 'plugindashboard',
|
||||||
|
defaultSize: { width: "1000px", height: "400px" },
|
||||||
|
icon: "plugins.png",
|
||||||
|
permissions: [ 'Administration' ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: 'System',
|
||||||
|
name: 'Info',
|
||||||
|
active: true,
|
||||||
|
menu: {
|
||||||
|
label: 'Info',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Info',
|
||||||
|
view: 'serverinfo',
|
||||||
|
defaultSize: { width: "900px", height: "500px" },
|
||||||
|
icon: "serverinfo.png",
|
||||||
|
permissions: [ 'Administration' ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: "Benutzer",
|
||||||
|
name: 'Einstellungen',
|
||||||
|
active: true,
|
||||||
|
menu: {
|
||||||
|
label: 'Einstellungen',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Einstellungen',
|
||||||
|
view: 'usersettings',
|
||||||
|
defaultSize: { width: "460px", height: "515px" },
|
||||||
|
icon: "app.png",
|
||||||
|
permissions: [ '*' ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: "Benutzer",
|
||||||
|
name: 'Hilfe',
|
||||||
|
active: true,
|
||||||
|
menu: {
|
||||||
|
label: 'Hilfe',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Hilfe',
|
||||||
|
view: 'help',
|
||||||
|
icon: "help.png",
|
||||||
|
permissions: [ '*' ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
40
src/models/notifyTrayModel.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
|
||||||
|
// // DB-Verbindung testen (optional)
|
||||||
|
// (async () => {
|
||||||
|
// try {
|
||||||
|
// await sequelize.authenticate();
|
||||||
|
// console.log(`Database connection established for NotifyTray`);
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error(`Unable to connect to the database`, error);
|
||||||
|
// }
|
||||||
|
// })();
|
||||||
|
|
||||||
|
const NotifyTray = sequelize.define('NotifyTray', {
|
||||||
|
ID: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true, // Identity
|
||||||
|
},
|
||||||
|
ObjectGUID: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
NotifyTrayObject_ID: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
SeenAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'NotifyTray',
|
||||||
|
timestamps: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NotifyTray;
|
||||||
|
};
|
||||||
39
src/models/notifyTrayObjectsModel.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const NotifyTrayObjects = sequelize.define('NotifyTrayObjects', {
|
||||||
|
ID: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true, // manuell zu vergeben
|
||||||
|
},
|
||||||
|
PluginName: {
|
||||||
|
type: DataTypes.STRING(50),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
Message: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
JSON: {
|
||||||
|
type: DataTypes.TEXT, // große JSON-Strings
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
ActionRequired: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
CreatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'NotifyTrayObjects',
|
||||||
|
timestamps: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NotifyTrayObjects;
|
||||||
|
};
|
||||||
70
src/models/notifyTrayView.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const NotifyTrayView = sequelize.define('vNotifyTray', {
|
||||||
|
ID: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
online: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
ObjectGUID: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
sAMAccountName: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
givenName: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
sn: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
mail: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
PluginName: {
|
||||||
|
type: DataTypes.STRING(50),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
JSON: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
ActionRequired: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
CreatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
Message: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
SeenAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'vNotifyTray', // 👈 WICHTIG: View statt Tabelle
|
||||||
|
timestamps: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NotifyTrayView;
|
||||||
|
};
|
||||||
24
src/models/pluginModel.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const Plugin = sequelize.define('Plugin', {
|
||||||
|
Name: {
|
||||||
|
type: DataTypes.STRING(50),
|
||||||
|
allowNull: false,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
Active: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
Version: {
|
||||||
|
type: DataTypes.STRING(25),
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'Plugins', // Tabellenname in der Datenbank
|
||||||
|
timestamps: false, // Falls du keine createdAt/updatedAt Spalten hast
|
||||||
|
});
|
||||||
|
|
||||||
|
return Plugin;
|
||||||
|
};
|
||||||
58
src/models/releasenotes.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"sAMAccountName": "manuel.sowada",
|
||||||
|
"plugin": "System",
|
||||||
|
"datetime": "2026-02-11",
|
||||||
|
"value": "HTML-Tabellen werden durch virtuelles Dataset abgebildet",
|
||||||
|
"finish": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sAMAccountName": "manuel.sowada",
|
||||||
|
"plugin": "System",
|
||||||
|
"datetime": "2026-04-14",
|
||||||
|
"value": "Bubble-Notify eingefügt",
|
||||||
|
"finish": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sAMAccountName": "manuel.sowada",
|
||||||
|
"plugin": "System",
|
||||||
|
"datetime": "2026-04-16",
|
||||||
|
"value": "Window drag 'n drop bugfixes",
|
||||||
|
"finish": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sAMAccountName": "manuel.sowada",
|
||||||
|
"plugin": "System",
|
||||||
|
"datetime": "2026-04-17",
|
||||||
|
"value": "OS-Logik und -Design Bugfixes + mobile-responsive-design",
|
||||||
|
"finish": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sAMAccountName": "manuel.sowada",
|
||||||
|
"plugin": "System",
|
||||||
|
"datetime": "2026-04-18",
|
||||||
|
"value": "Speichern der Window-Payloads pro Benutzer, divers Bugfixes und Verbesserungen in der OS-Logik",
|
||||||
|
"finish": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sAMAccountName": "manuel.sowada",
|
||||||
|
"plugin": "System",
|
||||||
|
"datetime": "2026-04-19",
|
||||||
|
"value": "PluginManager erweitert, Hilfe in Tabs umbgebaut, diverse Design-Bugfixes",
|
||||||
|
"finish": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sAMAccountName": "manuel.sowada",
|
||||||
|
"plugin": "System",
|
||||||
|
"datetime": "2026-04-20",
|
||||||
|
"value": "OS-Logik update: fetch ersetzt socket.io bei open_window und open_view",
|
||||||
|
"finish": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sAMAccountName": "manuel.sowada",
|
||||||
|
"plugin": "System",
|
||||||
|
"datetime": "2026-04-21",
|
||||||
|
"value": "Plugin-System + JSONtree bugfixes",
|
||||||
|
"finish": true
|
||||||
|
}
|
||||||
|
]
|
||||||
175
src/routes/adminRoutes.js
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
const { exec } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { service } = require('@root/server.js');
|
||||||
|
|
||||||
|
|
||||||
|
const configurationFile = path.join(require('@root/server.js').path.source, 'models', 'configuration.json');
|
||||||
|
const stylesheetFile = path.join(require('@root/server.js').path.source, 'models', 'stylesheet.json');
|
||||||
|
const serverInfoFile = path.join(require('@root/server.js').path.root, 'package.json');
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
route(app, authenticationManager, pluginManager, eventManager, socketManager, activeDirectoryManager, stylesheetJson) {
|
||||||
|
// JSON configuration abrufen
|
||||||
|
app.post('/api/getConfig', (req, res) => {
|
||||||
|
fs.readFile(configurationFile, 'utf8', (err, data) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.status(200).send(JSON.parse(data));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// JSON configuration speichern
|
||||||
|
app.post('/config', (req, res) => {
|
||||||
|
fs.writeFile(configurationFile, JSON.stringify(req.body, null, 2), (err) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.status(200).send({ status: 'ok' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// JSON stylesheet abrufen
|
||||||
|
app.post('/api/getStyles', (req, res) => {
|
||||||
|
fs.readFile(stylesheetFile, 'utf8', (err, data) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.status(200).send(JSON.parse(data));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// JSON stylesheet speichern
|
||||||
|
app.post('/style', (req, res) => {
|
||||||
|
fs.writeFile(stylesheetFile, JSON.stringify(req.body, null, 2), (err) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.status(200).send({ status: 'ok' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// JSON package.json abrufen
|
||||||
|
app.post('/api/getServerInfo', (req, res) => {
|
||||||
|
fs.readFile(serverInfoFile, 'utf8', (err, data) => {
|
||||||
|
if (err) { return res.status(500).send(err); }
|
||||||
|
res.status(200).send({ package: JSON.parse(data), pid: process.pid, releaseNotes: global.json.releaseNotes.live });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// JSON package.json speichern
|
||||||
|
app.post('/serverinfo', (req, res) => {
|
||||||
|
fs.writeFile(serverInfoFile, JSON.stringify(req.body, null, 2), (err) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.status(200).send({ status: 'ok' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
app.post('/api/eventlog/clearlog', (req, res) => {
|
||||||
|
eventManager.clear();
|
||||||
|
res.status(200).send({ status: 'ok' })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/eventlog/getLogs', async (req, res) => {
|
||||||
|
res.status(200).json(await service.get('eventManager').getAllEventLogs());
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
app.post('/api/shutdown', (req, res) => {
|
||||||
|
exec(`kill -9 ${process.pid}`, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
service.get('eventManager').write(req.cookies.ObjectGUID, 4, null, error.message);
|
||||||
|
return res.status(500).send({ status: 'error', message: error.message });
|
||||||
|
}
|
||||||
|
service.get('eventManager').write(req.cookies.ObjectGUID, 0, null, `Server heruntergefahren`);
|
||||||
|
res.status(200).send({ status: 'ok' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/restart', (req, res) => {
|
||||||
|
service.get('eventManager').write(req.cookies.ObjectGUID, 2, null, `Der Neustart ist noch nicht implementiert`);
|
||||||
|
res.status(200).send({ status: 'ok' });
|
||||||
|
// exec(`kill -9 ${process.pid}`, (error, stdout, stderr) => {
|
||||||
|
// if (error) {
|
||||||
|
// service.get('eventManager').write(req.cookies.ObjectGUID, 4, null, error.message);
|
||||||
|
// return res.status(500).send({ status: 'error', message: error.message });
|
||||||
|
// }
|
||||||
|
// service.get('eventManager').write(req.cookies.ObjectGUID, 0, null, `Server neu gestartet`);
|
||||||
|
// res.status(200).send({ status: 'ok' });
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
app.post('/api/plugins/activation', async (req, res) => {
|
||||||
|
const { name, state } = req.body;
|
||||||
|
let result = null;
|
||||||
|
if(state) {
|
||||||
|
result = await pluginManager.load(name, true);
|
||||||
|
} else {
|
||||||
|
result = await pluginManager.unload(name);
|
||||||
|
}
|
||||||
|
console.log(result)
|
||||||
|
// result = { ...result, authorized: result.metadata.permissions.some(async permission => { await activeDirectoryManager.getGroup(permission) != null && await activeDirectoryManager.isUserMemberOfRecursive(req.cookies.sAMAccountName, permission)}) }
|
||||||
|
|
||||||
|
eventManager.write(null, result.levelId, name, result.message);
|
||||||
|
// socketManager.broadcast('admin', 'plugin_status', result);
|
||||||
|
socketManager.broadcast('/', 'plugin_status', result);
|
||||||
|
res.status(200).json(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/plugins/getAll', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const plugins = await pluginManager.getStatus();
|
||||||
|
res.status(200).json(plugins);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
app.post('/api/plugins/:name/update', async (req, res) => {
|
||||||
|
const { name } = req.params;
|
||||||
|
try {
|
||||||
|
const { updates } = req.body;
|
||||||
|
const result = await pluginManager.update(name, updates);
|
||||||
|
// result = { ...result, authorized: result.metadata.permissions.some(async permission => { await activeDirectoryManager.getGroup(permission) != null && await activeDirectoryManager.isUserMemberOfRecursive(req.cookies.sAMAccountName, permission)}) }
|
||||||
|
|
||||||
|
service.get('eventManager').writeLog(req.cookies.ObjectGUID, result.levelId, name, result.message);
|
||||||
|
// socketManager.broadcast('admin', 'plugin_status', result);
|
||||||
|
// socketManager.broadcast('/', 'plugin_status', result);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
service.get('eventManager').write(req.cookies.ObjectGUID, 4, name, `Fehler beim Aktualisieren des Plugins: ${error}`);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/plugins/:name/rename', async (req, res) => {
|
||||||
|
const { name } = req.params;
|
||||||
|
const { newName } = req.body;
|
||||||
|
const result = await service.get('pluginManager').rename(name, newName);
|
||||||
|
|
||||||
|
// const result = { levelId: 0, pluginName: name, message: `Plugin erstellt` };
|
||||||
|
// await pluginManager.create(name);
|
||||||
|
// res.status(200).json(result);
|
||||||
|
|
||||||
|
eventManager.writeLog(null, result.levelId, name, result.message);
|
||||||
|
// socketManager.broadcast('admin', 'plugin_status', result);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/plugins/:name/create', async (req, res) => {
|
||||||
|
const { name } = req.params;
|
||||||
|
const result = { levelId: 0, pluginName: name, message: `Plugin erstellt` };
|
||||||
|
await pluginManager.create(name);
|
||||||
|
|
||||||
|
eventManager.writeLog(null, result.levelId, name, result.message);
|
||||||
|
res.status(200).json(result);
|
||||||
|
// socketManager.broadcast('admin', 'plugin_status', result);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/admin/plugins/:name/delete', async (req, res) => {
|
||||||
|
const { name } = req.params;
|
||||||
|
const result = { status: 'delete', pluginName: name, levelId: 0, message: `Plugin ${name} gelöscht` }; //await pluginManager.delete(name);
|
||||||
|
res.status(200).json(result);
|
||||||
|
|
||||||
|
eventManager.write(null, result.levelId, name, result.message);
|
||||||
|
socketManager.broadcast('admin', 'plugin_status', result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
125
src/routes/indexRoutes.js
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const { renderWindow } = require('@services/renderWindow.js');
|
||||||
|
const { service } = require('@root/server.js');
|
||||||
|
const { File: HotReload } = require(`@services/hotReload.js`);
|
||||||
|
|
||||||
|
const { doesNotReject } = require('assert');
|
||||||
|
// let startMenuItemContext = require('@models/integratedStartmenuItems.js')
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
route: function(app, service) {
|
||||||
|
app.get('/', service.get('authenticationManager').authenticate(), async (req, res) => {
|
||||||
|
const startMenuItems = await global.startMenuItems(
|
||||||
|
app,
|
||||||
|
req.cookies.sAMAccountName,
|
||||||
|
service
|
||||||
|
);
|
||||||
|
res.render('desktop', { layout: 'default', startMenuItems: startMenuItems });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
app.post('/api/open_app', (req, res) => {
|
||||||
|
const { name, view, viewLabel, location, size, state, zIndex } = req.body;
|
||||||
|
|
||||||
|
const pluginPath = path.join(global.path.plugins, name, 'plugin.json');
|
||||||
|
|
||||||
|
let context = fs.existsSync(pluginPath)
|
||||||
|
? service.get('fileSystemManager').loadJSON(pluginPath)
|
||||||
|
: (global.json.startMenuItems.live).find(item => item.name == name);
|
||||||
|
|
||||||
|
context.defaultSize =
|
||||||
|
context.menu.items.find(item => item.label == viewLabel)?.defaultSize ||
|
||||||
|
{ width: 800, height: 600 };
|
||||||
|
|
||||||
|
delete context.config;
|
||||||
|
res.json({ name, view, viewLabel, context, location, size, state, zIndex });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app.get('/api/NotifyTray/getTrays', async (req, res) => {
|
||||||
|
const objectGuid = req.cookies.ObjectGUID;
|
||||||
|
// console.log(await service.get('notifyTray').getOpenNotifications(objectGuid))
|
||||||
|
res.status(200).json(await service.get('notifyTray').getOpenNotifications(objectGuid));
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/NotifyTray/markAsSeen', async (req, res) => {
|
||||||
|
const objectGuid = req.cookies.ObjectGUID;
|
||||||
|
const notificationId = req.body.id;
|
||||||
|
const notificationValue = req.body.value;
|
||||||
|
await service.get('notifyTray').markAsSeen(objectGuid, notificationId, notificationValue);
|
||||||
|
res.status(204).send();
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/Plugins/loadScripts', service.get('authenticationManager').authenticate(), async(req, res) => {
|
||||||
|
const scripts = service.get('pluginManager').getStatus().map(plugin => {
|
||||||
|
|
||||||
|
const exists = service.get('fileSystemManager').exists(path.join(plugin.pluginPath, 'public', 'javascript', 'main.js'))
|
||||||
|
if (exists && exists.status) {
|
||||||
|
return path.join('/', plugin.name, 'javascript', 'main.js');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter(script => script !== null);
|
||||||
|
res.status(200).send(scripts);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app.post('/window/EventLog/eventlog', async (req, res) => {
|
||||||
|
await renderWindow(app, 'eventlog.hbs', { }, { }, res)
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/window/Server/styleconfig', async (req, res) => {
|
||||||
|
await renderWindow(app, 'styleconfig.hbs', { }, { }, res)
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/window/Plugins/plugindashboard', async (req, res) => {
|
||||||
|
await renderWindow(app, 'plugindashboard.hbs', { }, { }, res)
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/window/Info/serverinfo', async (req, res) => {
|
||||||
|
await renderWindow(app, 'serverinfo.hbs', { }, { }, res)
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/window/Server/serverconfig', async (req, res) => {
|
||||||
|
await renderWindow(app, 'serverconfig.hbs', { }, { }, res)
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/window/Einstellungen/usersettings', async (req, res) => {
|
||||||
|
await renderWindow(app, 'usersettings.hbs', { }, { }, res)
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/window/Hilfe/help', async (req, res) => {
|
||||||
|
await renderWindow(app, 'help.hbs', { }, { }, res)
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/help/getTabs', async (req, res) => {
|
||||||
|
const tabNames = (await startMenuItems(app, req.cookies.sAMAccountName))
|
||||||
|
.filter(plugin => plugin.active && plugin.menu.items.some(i => i.authorized))
|
||||||
|
.map(plugin => ({ name: plugin.menu.label }));
|
||||||
|
res.status(200).json(tabNames);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/help/getHelp', async (req, res) => {
|
||||||
|
const { name } = req.body;
|
||||||
|
const props = (await startMenuItems(app, req.cookies.sAMAccountName))
|
||||||
|
.filter(plugin => plugin.name === name && plugin.active && plugin.menu.items.some(i => i.authorized))
|
||||||
|
.map(async plugin => (
|
||||||
|
{
|
||||||
|
name: plugin.menu.label,
|
||||||
|
version: plugin.version,
|
||||||
|
description: plugin.description,
|
||||||
|
html: await fs.promises.readFile(plugin.section === 'Plugin' ? path.join(plugin.pluginPath, 'docs', 'help.html') : path.join(app.locals.path.public, 'views', 'help', plugin.name + '.html'), 'utf-8')
|
||||||
|
}
|
||||||
|
));
|
||||||
|
res.status(200).send(await props[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
79
src/routes/loginRoutes.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
const { verify } = require("jsonwebtoken");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
route(app, authenticationManager, socketManager, eventManager) {
|
||||||
|
app.get(`/login`, (req, res) => {
|
||||||
|
res.render(`login`, { layout: 'default' });
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/login', async (req, res) => {
|
||||||
|
const { sAMAccountName, password } = req.body;
|
||||||
|
const userModel = await authenticationManager.Authentication.findOne({
|
||||||
|
where: { sAMAccountName: sAMAccountName }, attributes: ['ObjectGUID'],
|
||||||
|
raw: true
|
||||||
|
});
|
||||||
|
const objectGuid = userModel !== null ? userModel.ObjectGUID : sAMAccountName;
|
||||||
|
try {
|
||||||
|
// set safety cookies
|
||||||
|
res.cookie('sAMAccountName', sAMAccountName, {
|
||||||
|
httpOnly: false,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'Strict',
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 365
|
||||||
|
})
|
||||||
|
|
||||||
|
res.cookie('ObjectGUID', objectGuid, {
|
||||||
|
httpOnly: false,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'Strict',
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 365
|
||||||
|
})
|
||||||
|
const login = await authenticationManager.login(sAMAccountName, password);
|
||||||
|
|
||||||
|
eventManager.writeLog(objectGuid, login.levelId, null, login.message);
|
||||||
|
res.status(login.levelId == 0 ? 200 : 401).json(login);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json(login);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Geschützte Route
|
||||||
|
app.get('/me', authenticationManager.authenticate(), (req, res) => {
|
||||||
|
res.json(JSON.stringify({
|
||||||
|
user: {
|
||||||
|
name: req.user
|
||||||
|
}
|
||||||
|
}, null, 4));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/checkLoginName', async (req, res) => {
|
||||||
|
const { sAMAccountName } = req.body;
|
||||||
|
|
||||||
|
const userExists = await authenticationManager.Authentication.findOne({ where: { sAMAccountName: sAMAccountName } });
|
||||||
|
const auth = { objectGuid: userExists != null ? userExists.ObjectGUID : sAMAccountName, sAMAccountName: sAMAccountName };
|
||||||
|
res.status(userExists ? 200 : 404).json({ exists: userExists != null });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/verifying', async (req, res, next) => {
|
||||||
|
const verify = await authenticationManager.verifyUserToken();
|
||||||
|
eventManager.write(req.user.objectGuid, verify.levelId, null, verify.message);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
app.post('/logout', authenticationManager.authenticate(), async (req, res) => {
|
||||||
|
const logout = await authenticationManager.logout(req.user.sAMAccountName);
|
||||||
|
|
||||||
|
socketManager.sendTo('/', req.user.objectGuid, 'login_status', { levelId: logout.levelId, message: logout.message } )
|
||||||
|
eventManager.write(req.user.objectGuid, logout.levelId, null, logout.message);
|
||||||
|
|
||||||
|
res.clearCookie('sAMAccountName');
|
||||||
|
res.clearCookie('ObjectGUID');
|
||||||
|
|
||||||
|
setTimeout(() => res.render('login', { layout: false, title: app.locals.configuration.server.name }), 3000);
|
||||||
|
// res.json({ message: 'Logout erfolgreich' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
258
src/services/activeDirectoryManager.js
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
const ActiveDirectory = require('activedirectory2');
|
||||||
|
|
||||||
|
class ActiveDirectoryManager {
|
||||||
|
constructor({
|
||||||
|
url,
|
||||||
|
baseDN,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
userAttributes,
|
||||||
|
groupAttributes,
|
||||||
|
computerAttributes
|
||||||
|
}) {
|
||||||
|
this.ad = new ActiveDirectory({
|
||||||
|
url,
|
||||||
|
baseDN,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
attributes: {
|
||||||
|
user: userAttributes,
|
||||||
|
group: groupAttributes,
|
||||||
|
computer: computerAttributes
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userAttributes = userAttributes;
|
||||||
|
this.groupAttributes = groupAttributes;
|
||||||
|
this.computerAttributes = computerAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* INTERNAL GENERIC LDAP SEARCH
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
async ldapSearch(options) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.ad.find(options, (err, result) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(result || {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* USER FUNCTIONS
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
async getUser(username, attributes = this.userAttributes) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.ad.findUser({ attributes }, username, (err, user) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(user || null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserDN(username) {
|
||||||
|
const user = await this.getUser(username);
|
||||||
|
return user?.dn || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findUsers(query, attributes = this.userAttributes) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const filter = `(&(objectClass=user)(|(cn=${query})(sAMAccountName=${query})(mail=${query})(displayName=${query})))`;
|
||||||
|
|
||||||
|
this.ad.findUsers({ filter, attributes }, (err, users) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(users || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* GROUP FUNCTIONS
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
async getGroup(groupName, attributes = this.groupAttributes) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.ad.findGroup({ attributes }, groupName, (err, group) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(group || null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findGroups(query, attributes = this.groupAttributes) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const filter = `(&(objectClass=group)(cn=${query}))`;
|
||||||
|
|
||||||
|
this.ad.findGroups({ filter, attributes }, (err, groups) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(groups || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* COMPUTER / OU FUNCTIONS 🖥️
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einzelnen Computer holen
|
||||||
|
*/
|
||||||
|
async getComputer(name, attributes = this.computerAttributes) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const filter = `(&(objectClass=computer)(|(cn=${name})(dNSHostName=${name})))`;
|
||||||
|
|
||||||
|
this.ad.find({ filter, attributes }, (err, result) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(result?.other?.[0] || null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alle Computer
|
||||||
|
*/
|
||||||
|
async getComputers(attributes = this.computerAttributes) {
|
||||||
|
const options = {
|
||||||
|
baseDN: this.ad.baseDN,
|
||||||
|
filter: '(objectClass=computer)',
|
||||||
|
attributes
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.ldapSearch(options);
|
||||||
|
return result.other || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alle Computer aus einer OU holen
|
||||||
|
*/
|
||||||
|
async getComputersFromOU(ouDn, attributes = this.computerAttributes) {
|
||||||
|
const options = {
|
||||||
|
baseDN: ouDn,
|
||||||
|
filter: '(objectClass=computer)',
|
||||||
|
attributes
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.ldapSearch(options);
|
||||||
|
return result.other || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computer suchen (Wildcard möglich)
|
||||||
|
* Beispiele: "PC-*", "*LAPTOP*", "SRV01"
|
||||||
|
*/
|
||||||
|
async findComputers(query, attributes = this.computerAttributes) {
|
||||||
|
const filter = `(&(objectClass=computer)(|(cn=${query})(dNSHostName=${query})))`;
|
||||||
|
|
||||||
|
const result = await this.ldapSearch({
|
||||||
|
filter,
|
||||||
|
attributes
|
||||||
|
});
|
||||||
|
return result.other || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* GROUP MEMBERSHIP (DIRECT & RECURSIVE)
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
async isUserMemberOfDirect(username, groupName) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.ad.isUserMemberOf(username, groupName, (err, isMember) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(isMember);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async isUserMemberOfRecursive(username, groupName, visited = new Set()) {
|
||||||
|
const key = groupName.toLowerCase();
|
||||||
|
if (visited.has(key)) return false;
|
||||||
|
visited.add(key);
|
||||||
|
|
||||||
|
const direct = await this.isUserMemberOfDirect(username, groupName);
|
||||||
|
if (direct) return true;
|
||||||
|
|
||||||
|
const group = await this.getGroup(groupName);
|
||||||
|
if (!group || !Array.isArray(group.member)) return false;
|
||||||
|
|
||||||
|
for (const dn of group.member) {
|
||||||
|
const match = dn.match(/CN=([^,]+)/i);
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
const subGroupName = match[1];
|
||||||
|
const found = await this.isUserMemberOfRecursive(username, subGroupName, visited);
|
||||||
|
if (found) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroupSubgroups(groupName, visited = new Set()) {
|
||||||
|
const key = groupName.toLowerCase();
|
||||||
|
if (visited.has(key)) return [];
|
||||||
|
|
||||||
|
visited.add(key);
|
||||||
|
|
||||||
|
const group = await this.getGroup(groupName);
|
||||||
|
if (!group || !Array.isArray(group.member)) return [];
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const memberDN of group.member) {
|
||||||
|
const match = memberDN.match(/CN=([^,]+)/i);
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
const subGroupName = match[1];
|
||||||
|
const sub = await this.getGroup(subGroupName).catch(() => null);
|
||||||
|
if (!sub) continue;
|
||||||
|
|
||||||
|
results.push(sub);
|
||||||
|
results.push(...await this.getGroupSubgroups(subGroupName, visited));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroupRecursive(groupName, visited = new Set()) {
|
||||||
|
const key = groupName.toLowerCase();
|
||||||
|
if (visited.has(key)) return null;
|
||||||
|
|
||||||
|
visited.add(key);
|
||||||
|
|
||||||
|
const group = await this.getGroup(groupName);
|
||||||
|
if (!group) return null;
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
...group,
|
||||||
|
subgroups: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!Array.isArray(group.member)) return result;
|
||||||
|
|
||||||
|
for (const memberDN of group.member) {
|
||||||
|
const match = memberDN.match(/CN=([^,]+)/i);
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
const subGroupName = match[1];
|
||||||
|
const subTree = await this.getGroupRecursive(subGroupName, visited);
|
||||||
|
if (subTree) result.subgroups.push(subTree);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ActiveDirectoryManager;
|
||||||
172
src/services/authenticationManager.js
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
|
let { levelId, message } = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication class for login method, token validation and password setting
|
||||||
|
*/
|
||||||
|
class AuthenticationManager {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} model - Use the authentication database model for interact with the database
|
||||||
|
* @param {string} secretKey - Defines the server secret for token validation
|
||||||
|
*/
|
||||||
|
constructor(model, secretKey, eventManager) {
|
||||||
|
this.eventManager = eventManager;
|
||||||
|
|
||||||
|
// if (!model) throw new Error('Sequelize Model wird benötigt');
|
||||||
|
// if (!secretKey) throw new Error('Secret Key wird benötigt');
|
||||||
|
|
||||||
|
this.Authentication = model;
|
||||||
|
this.SECRET_KEY = secretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set or reset password of user
|
||||||
|
* @param {string} sAMAccountName - Windows account name
|
||||||
|
* @param {string} password - Set the new password
|
||||||
|
*/
|
||||||
|
async setPassword(sAMAccountName, password) {
|
||||||
|
const user = await this.Authentication.findOne({ where: { sAMAccountName } });
|
||||||
|
if (!user) {
|
||||||
|
// this.eventManager.write(null, 2, 0, { aboveLevel: 1 }, `User nicht gefunden`);
|
||||||
|
levelId = 2;
|
||||||
|
message = `Unbekannter User`
|
||||||
|
return {token: null, levelId: levelId };
|
||||||
|
// throw new Error(`User ${sAMAccountName} nicht gefunden`);
|
||||||
|
}
|
||||||
|
// if (user.password) throw new Error('Passwort bereits gesetzt');
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
user.password = hashedPassword;
|
||||||
|
await user.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login mit Speicherung des Tokens in der Datenbank
|
||||||
|
*/
|
||||||
|
async login(sAMAccountName, password) {
|
||||||
|
const user = await this.Authentication.findOne({ where: { sAMAccountName } });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
//this.eventManager.write(null, 2, null, null, `User ${sAMAccountName} nicht geufunden`)
|
||||||
|
levelId = 2;
|
||||||
|
message = `Unbekannter Benutzer`;
|
||||||
|
return { token: null, levelId: levelId, message: message };
|
||||||
|
// throw new Error('Unkown user');
|
||||||
|
}
|
||||||
|
if (!user.password) {
|
||||||
|
this.setPassword(sAMAccountName, password);
|
||||||
|
// this.eventManager.write(user.ObjectGUID, 2, null, null, 'User registration initialized')
|
||||||
|
levelId = 1;
|
||||||
|
message = `Benutzer nicht registiert`;
|
||||||
|
return { token: null, levelId: levelId, message: message };
|
||||||
|
// throw new Error('User not registered');
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordMatch = await bcrypt.compare(password, user.password);
|
||||||
|
if (!passwordMatch) {
|
||||||
|
// this.eventManager.write(user.ObjectGUID, 2, null, null, 'Password doesn\'t match');
|
||||||
|
levelId = 2;
|
||||||
|
message = `Falsches Passwort`;
|
||||||
|
return { token: null, levelId: levelId, message: message };
|
||||||
|
// throw new Error('Wrong password');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token erzeugen
|
||||||
|
const payload = {
|
||||||
|
sAMAccountName: user.sAMAccountName,
|
||||||
|
mail: user.mail,
|
||||||
|
givenName: user.givenName,
|
||||||
|
sn: user.sn
|
||||||
|
};
|
||||||
|
|
||||||
|
const token = jwt.sign(payload, this.SECRET_KEY, { expiresIn: '100y' });
|
||||||
|
// Token in DB speichern
|
||||||
|
user.refreshtoken = token;
|
||||||
|
user.online = true;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
// this.eventManager.write(user.ObjectGUID, 1, null, null, 'Erfolgreich angemeldet');
|
||||||
|
levelId = 0;
|
||||||
|
message = `Erfolgreich angemeldet`;
|
||||||
|
return { token: token, levelId: levelId, message: message };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout löscht Token aus der DB
|
||||||
|
*/
|
||||||
|
async logout(sAMAccountName) {
|
||||||
|
const user = await this.Authentication.findOne({ where: { sAMAccountName } });
|
||||||
|
if (user) {
|
||||||
|
user.refreshtoken = null;
|
||||||
|
user.online = false;
|
||||||
|
await user.save();
|
||||||
|
levelId = 0;
|
||||||
|
message = `Erfolgreich abgemeldet`;
|
||||||
|
return { token: null, levelId: levelId, message: message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token-Prüfung (über DB)
|
||||||
|
*/
|
||||||
|
async verifyUserToken(sAMAccountName) {
|
||||||
|
const user = await this.Authentication.findOne({ where: { sAMAccountName } });
|
||||||
|
if (!user || !user.refreshtoken) {
|
||||||
|
levelId = 1,
|
||||||
|
message = `Kein gültiger Token`;
|
||||||
|
// throw new Error('Kein gespeicherter Token gefunden');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = jwt.verify(user.refreshtoken, this.SECRET_KEY);
|
||||||
|
levelId = 0;
|
||||||
|
message = `User verifiziert`;
|
||||||
|
return { valid: true, payload, user, levelId: levelId, message: message }
|
||||||
|
} catch {
|
||||||
|
levelId = 4;
|
||||||
|
message = `Ungültiger Token`;
|
||||||
|
return { valid: false, payload, user, levelId: levelId, message: message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Express Middleware – prüft Token direkt aus DB anhand sAMAccountNamec
|
||||||
|
*/
|
||||||
|
authenticate() {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const sAMAccountName = req.cookies?.sAMAccountName;
|
||||||
|
const objectGUID = req.cookies?.ObjectGUID;
|
||||||
|
if (!sAMAccountName || !objectGUID) {
|
||||||
|
return res.redirect('/login');
|
||||||
|
// return res.status(401).json({ message: 'Kein Benutzer-Cookie gefunden' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.Authentication.findOne({ where: { sAMAccountName } });
|
||||||
|
if (!user || !user.refreshtoken) {
|
||||||
|
return res.redirect('/login');
|
||||||
|
// return res.status(401).json({ message: 'Benutzer oder Token nicht gefunden' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.active === false) {
|
||||||
|
return res.redirect('/login');
|
||||||
|
// return res.status(401).json({ message: 'Benutzer ist nicht aktiv' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token aus DB prüfen
|
||||||
|
const payload = jwt.verify(user.refreshtoken, this.SECRET_KEY);
|
||||||
|
req.user = user;
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.redirect('/login');
|
||||||
|
// res.status(401).json({ message: 'Authentifizierung fehlgeschlagen' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AuthenticationManager;
|
||||||
159
src/services/eventManager.js
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
const { Op } = require('sequelize');
|
||||||
|
/**
|
||||||
|
* Custom event logging class
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class EventManager {
|
||||||
|
/**
|
||||||
|
* @param {*} eventlogModel - Use the eventlog database model for interact with the database
|
||||||
|
* @param {*} socketManager - Get the administration socket for sending events to frontend
|
||||||
|
*/
|
||||||
|
constructor(app, eventlogModel, eventLogView, socketManager) {
|
||||||
|
this.app = app;
|
||||||
|
this.EventLog = eventlogModel;
|
||||||
|
this.socketManager = socketManager;
|
||||||
|
this.EventLogView = eventLogView;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new log entry in EventLog database table
|
||||||
|
* @param {string} [objectGuid] - MSSQL uniqueidentifier (UUID) or null = '00000000-0000-0000-0000-000000000000' AS 'SYSTEM'
|
||||||
|
* @param {number} levelId - -1=test, 0=success, 1=log, 2=warn, 4=error, 8=throw_exception
|
||||||
|
* @param {number} pluginId - ID of plugin
|
||||||
|
* @param {Object} socketSending - Use socket to send error message to admin web frontends. Object: { active: true, levelId: 1 }
|
||||||
|
* @param {Array} args - Message: string or comma seperated array of string
|
||||||
|
* args[0].stack !== undefined - => Client side (error) stack and message
|
||||||
|
*/
|
||||||
|
async write(objectGuid, levelId, pluginName = null, ...args) {
|
||||||
|
const err = new Error();
|
||||||
|
const stackLine = args[0].stack !== undefined ? args[0].stack : err.stack.split('\n')[2]; // calls trace-line
|
||||||
|
//const trace = stackLine.split("\n") !== undefined ? stackLine.split("\n")[1].trim() : stackLine.match(/\/.*\d+/)[0].replace(this.app.locals.path.root, '') || ''; // path:line:column
|
||||||
|
const trace = stackLine?.split("\n")?.[1]?.trim() ?? stackLine?.match(/\/.*\d+/)?.[0]?.replace(this.app.locals.path.root, '') ?? '';
|
||||||
|
|
||||||
|
// const message = !Array.isArray(...args) ? [...args][0] : [...args][0][0]
|
||||||
|
const message = args[0].stack !== undefined ? args[0].message : args.join('\r\n\t');
|
||||||
|
|
||||||
|
const convertLevel = levelId == -1 ? 'test' : levelId == 0 ? 'success' : levelId == 1 ? 'log' : levelId == 2 ? 'warn' : levelId == 4 ? 'error' : 'throw exception';
|
||||||
|
const convertPluginName = pluginName == null ? 'SYSTEM' : pluginName;
|
||||||
|
|
||||||
|
const entry = await this.EventLog.create({
|
||||||
|
Message: message,
|
||||||
|
Trace: trace,
|
||||||
|
Level_ID: levelId,
|
||||||
|
PluginName: convertPluginName,
|
||||||
|
ObjectGUID: (objectGuid == null || objectGuid === undefined ? '00000000-0000-0000-0000-000000000000' : objectGuid)
|
||||||
|
});
|
||||||
|
const newLogEntry = await this.EventLogView.findOne( { where: { ID: entry.ID }, plain: true } );
|
||||||
|
if(levelId > -1) { // if not levelId = -1 | test-message
|
||||||
|
this.socketManager.broadcast('admin', 'eventlog_table', newLogEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.app.locals.configuration.debug.active && levelId >= this.app.locals.configuration.debug.levelId) {
|
||||||
|
this.socketManager.broadcast('admin', 'eventlog', { levelId: levelId, pluginName: pluginName, datetime: `[${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}]`, trace: `[${trace}]`, message: message })
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}][${convertPluginName}][${convertLevel}][${trace}]:`, message.replaceAll('<br>', '\r\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only creates a new log entry in EventLog database table, without sending socketEvent
|
||||||
|
* @param {string} [objectGuid] - MSSQL uniqueidentifier (UUID) or null = '00000000-0000-0000-0000-000000000000' AS 'SYSTEM'
|
||||||
|
* @param {number} levelId - -1=test, 0=success, 1=log, 2=warn, 4=error, 8=throw_exception
|
||||||
|
* @param {number} pluginId - ID of plugin
|
||||||
|
* @param {Object} socketSending - Use socket to send error message to admin web frontends. Object: { active: true, levelId: 1 }
|
||||||
|
* @param {Array} args - Message: string or comma seperated array of string
|
||||||
|
* args[0].stack !== undefined - => Client side (error) stack and message
|
||||||
|
*/
|
||||||
|
async writeLog(objectGuid, levelId, pluginName = null, ...args) {
|
||||||
|
const err = new Error();
|
||||||
|
const stackLine = args[0].stack !== undefined ? args[0].stack : err.stack.split('\n')[2]; // calls trace-line
|
||||||
|
const trace = args[0].stack !== undefined ? stackLine.split("\n")[1].trim() : stackLine.match(/\/.*\d+/)[0].replace(this.app.locals.path.root, ''); // path:line:column
|
||||||
|
// const message = !Array.isArray(...args) ? [...args][0] : [...args][0][0]
|
||||||
|
const message = args[0].stack !== undefined ? args[0].message : args.join('\r\n\t');
|
||||||
|
|
||||||
|
const convertLevel = levelId == -1 ? 'test' : levelId == 0 ? 'success' : levelId == 1 ? 'log' : levelId == 2 ? 'warn' : levelId == 4 ? 'error' : 'throw exception';
|
||||||
|
const convertPluginName = pluginName == null ? 'SYSTEM' : pluginName;
|
||||||
|
|
||||||
|
const entry = await this.EventLog.create({
|
||||||
|
Message: message,
|
||||||
|
Trace: trace,
|
||||||
|
Level_ID: levelId,
|
||||||
|
PluginName: convertPluginName,
|
||||||
|
ObjectGUID: (objectGuid == null || objectGuid === undefined ? '00000000-0000-0000-0000-000000000000' : objectGuid)
|
||||||
|
});
|
||||||
|
|
||||||
|
const newLogEntry = await this.EventLogView.findOne( { where: { ID: entry.ID }, plain: true } );
|
||||||
|
if(levelId > -1) { // if not levelId = -1 | test-message
|
||||||
|
this.socketManager.broadcast('admin', 'eventlog_table', newLogEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}][${convertPluginName}][${convertLevel}][${trace}]:`, message.replaceAll('<br>', '\r\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the eventlog database table
|
||||||
|
*/
|
||||||
|
async clear() {
|
||||||
|
const err = new Error();
|
||||||
|
const stackLine = err.stack.split('\n')[2]; // calls trace-line
|
||||||
|
const trace = stackLine.match(/\/.*\d+/)[0].replace(this.app.locals.path.root, ''); // path:line:column
|
||||||
|
|
||||||
|
const message = `${this.EventLog.tableName} cleared successfully`;
|
||||||
|
await this.EventLog.destroy({
|
||||||
|
where: {},
|
||||||
|
truncate: true,
|
||||||
|
});
|
||||||
|
this.socketManager.broadcast('admin', 'eventlog', { levelId: 0, pluginName: 'SYSTEM', datetime: `[${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}]`, trace: `[${trace}]`, message: message })
|
||||||
|
console.log(`[${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}][${trace}]`, message.replaceAll('<br>', '\r\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all eventlogs from database table
|
||||||
|
*/
|
||||||
|
async getAllEventLogs() {
|
||||||
|
try {
|
||||||
|
const logs = await this.EventLogView.findAll({ order: [['ID', 'DESC']] }); // Alle Zeilen abrufen
|
||||||
|
const logsArray = logs.map(log => log.get({ plain: true })); // Sequelize-Objekte in reine JS-Objekte
|
||||||
|
|
||||||
|
return logsArray; // Rückgabe als Array
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Abrufen der EventLogs:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specified range of eventlogs from database table
|
||||||
|
*/
|
||||||
|
async getEventLogs(fromID = null, toID) {
|
||||||
|
try {
|
||||||
|
const whereStatement = fromID != null ? {
|
||||||
|
where: {
|
||||||
|
ID: {
|
||||||
|
[Op.gte]: fromID, // größer gleich vonID
|
||||||
|
[Op.lte]: toID // kleiner gleich bisID
|
||||||
|
}
|
||||||
|
},
|
||||||
|
order: [['ID', 'DESC']]
|
||||||
|
} :
|
||||||
|
{
|
||||||
|
limit: toID,
|
||||||
|
order: [['ID', 'DESC']]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const logs = await this.EventLogView.findAll(whereStatement);
|
||||||
|
|
||||||
|
const logsArray = logs.map(log => log.get({ plain: true }));
|
||||||
|
return logsArray;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Abrufen der EventLogs:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = EventManager;
|
||||||
187
src/services/fileSystemManager.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class FileSystemManager {
|
||||||
|
/**
|
||||||
|
* @param {string} jsonPath - Pfad zur JSON-Datei
|
||||||
|
* @param {boolean} watch - ob die JSON-Datei überwacht werden soll (Live-Update)
|
||||||
|
*/
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
loadAllFiles(path, fileextension = null) {
|
||||||
|
let files = {};
|
||||||
|
fs.readdirSync(path).forEach(file => {
|
||||||
|
if (fileextension == null || file.endsWith(fileextension)) {
|
||||||
|
const filefound = require(`${path}/${file}`);
|
||||||
|
files = { ...files, ...filefound }; // zusammenführen
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllFiles(path, fileextension = null) {
|
||||||
|
let files = {};
|
||||||
|
fs.readdirSync(path).forEach(file => {
|
||||||
|
if (fileextension == null || file.endsWith(fileextension)) {
|
||||||
|
const filefound = `${path}/${file}`;
|
||||||
|
files = { ...files, ...filefound }; // zusammenführen
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest rekursiv Dateien und gibt nur die gewünschten Attribute zurück.
|
||||||
|
*
|
||||||
|
* @param {string} dirPath - Startverzeichnis
|
||||||
|
* @param {string[]} attributes - gewünschte Attribute:
|
||||||
|
* ['name','fullPath','size','lastModified','isDirectory','extension', ...]
|
||||||
|
* @param {string|null} sortBy - Attribut zum Sortieren (z.B. 'lastModified' oder 'name')
|
||||||
|
* @param {string} order - 'asc' oder 'desc'
|
||||||
|
*/
|
||||||
|
readFiles(dirPath, attributes = ['name', 'fullPath'], sortBy = null, order = 'asc') {
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
const items = fs.readdirSync(dirPath);
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const fullPath = path.join(dirPath, item);
|
||||||
|
const stats = fs.statSync(fullPath);
|
||||||
|
const isDir = stats.isDirectory();
|
||||||
|
|
||||||
|
// Objekt mit ALLEN möglichen Infos
|
||||||
|
const allInfo = {
|
||||||
|
name: item,
|
||||||
|
nameWithoutExt: item.substring(0, item.indexOf('.')),
|
||||||
|
fullPath: fullPath,
|
||||||
|
size: stats.size,
|
||||||
|
lastModified: stats.mtime,
|
||||||
|
created: stats.birthtime,
|
||||||
|
isDirectory: isDir,
|
||||||
|
extension: isDir ? null : path.extname(item)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isDir) {
|
||||||
|
// rekursiv weitermachen
|
||||||
|
results = results.concat(this.readFiles(fullPath, attributes, null, order));
|
||||||
|
} else {
|
||||||
|
// nur gewünschte Attribute ausgeben
|
||||||
|
const filtered = {};
|
||||||
|
for (const attr of attributes) {
|
||||||
|
if (allInfo[attr] !== undefined) {
|
||||||
|
filtered[attr] = allInfo[attr];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results.push(filtered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sortieren, falls gewünscht
|
||||||
|
if (sortBy) {
|
||||||
|
results.sort((a, b) => {
|
||||||
|
if (a[sortBy] < b[sortBy]) return order === 'asc' ? -1 : 1;
|
||||||
|
if (a[sortBy] > b[sortBy]) return order === 'asc' ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sammelt verschiedene Pattern-Ergebnisse aus Dateien mehrerer Ordner.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {string|string[]} options.folderPaths - Pfad oder Array von Pfaden zu Ordnern
|
||||||
|
* @param {string} [options.extension='.js'] - Dateiendung
|
||||||
|
* @param {Array<{ name: string, pattern: RegExp, mapFn?: Function }>} options.patterns - Liste von Pattern-Definitionen
|
||||||
|
* @param {boolean} [options.recursive=true] - Unterordner durchsuchen?
|
||||||
|
* @returns {Array} - kombinierte Ergebnisse aller Pattern
|
||||||
|
*/
|
||||||
|
collectFromFiles({
|
||||||
|
folderPaths,
|
||||||
|
extension = '.js',
|
||||||
|
patterns,
|
||||||
|
recursive = true
|
||||||
|
}) {
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
// if only one path selected
|
||||||
|
const paths = Array.isArray(folderPaths) ? folderPaths : [folderPaths];
|
||||||
|
|
||||||
|
function readDir(dir) {
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory() && recursive) {
|
||||||
|
readDir(fullPath);
|
||||||
|
} else if (entry.isFile() && entry.name.endsWith(extension)) {
|
||||||
|
const content = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
|
||||||
|
// 👉 NEU: fallback wenn keine patterns
|
||||||
|
if (!patterns || patterns.length === 0) {
|
||||||
|
results.push({
|
||||||
|
file: entry.name,
|
||||||
|
fullPath
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { name, pattern, mapFn } of patterns) {
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(content)) !== null) {
|
||||||
|
const mapped = mapFn
|
||||||
|
? mapFn(match, entry.name, fullPath, name)
|
||||||
|
: { file: entry.name, type: name, match: match[0] };
|
||||||
|
results.push(mapped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run through multiple paths
|
||||||
|
for (const dir of paths) {
|
||||||
|
if (fs.existsSync(dir)) {
|
||||||
|
readDir(path.resolve(dir));
|
||||||
|
} else {
|
||||||
|
console.warn(`Ordner nicht gefunden: ${dir}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// JSON-Datei laden
|
||||||
|
loadJSON(path) {
|
||||||
|
try {
|
||||||
|
const rawData = fs.readFileSync(path, 'utf8');
|
||||||
|
return JSON.parse(rawData);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Check file-/path
|
||||||
|
exists(path) {
|
||||||
|
try {
|
||||||
|
const info = fs.statSync(path);
|
||||||
|
if (!info.isFile() && !info.isDirectory()) return { status: false, levelId: 4, message: `${info.isFile() ? 'Datei' : 'Pfad'} ${path} existiert nicht` };
|
||||||
|
return { status: true, levelId: 0, message: `${info.isFile() ? 'Datei' : 'Pfad'} existiert: ${path}` };
|
||||||
|
} catch (err) {
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FileSystemManager;
|
||||||
377
src/services/hotReload.js
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
// hotReloadJson.js
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { watch } = require('chokidar');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
File: class {
|
||||||
|
constructor(filePath, options = {}) {
|
||||||
|
if (!filePath || typeof filePath !== 'string') {
|
||||||
|
return { levelId: 4, pluginName: null, message: 'filePath muss ein gültiger String sein' };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filePath = path.resolve(filePath);
|
||||||
|
this.fileType = options.fileType || "json"; // "json" oder "js"
|
||||||
|
this.historyLimit = options.historyLimit || 50;
|
||||||
|
this.autoCreate = options.autoCreate || false;
|
||||||
|
this.autoSaveJs = options.autoSaveJs || false; // neu: JS-Dateien optional speichern
|
||||||
|
this.callbacks = [];
|
||||||
|
this.history = [];
|
||||||
|
this.redoStack = [];
|
||||||
|
|
||||||
|
if (!fs.existsSync(this.filePath)) {
|
||||||
|
if(this.autoCreate) {
|
||||||
|
fs.writeFileSync(this.filePath, this.fileType === 'json' ? '{ }' : 'module.exports = {}');
|
||||||
|
} else {
|
||||||
|
return { levelId: 4, pluginName: null, message: `Datei existiert nicht: ${this.filePath}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialData = this.loadFile();
|
||||||
|
if (!initialData) {
|
||||||
|
this.data = {};
|
||||||
|
return { levelId: 4, pluginName: null, message: `Fehler beim Laden der Datei: ${this.filePath}` };
|
||||||
|
} else {
|
||||||
|
this.data = initialData;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.proxy = this.createProxy(this.data);
|
||||||
|
this.pushHistory(null, this.deepCopy(this.data));
|
||||||
|
|
||||||
|
this.watcher = watch(this.filePath, { ignoreInitial: true });
|
||||||
|
this.watcher.on('change', () => this.reload());
|
||||||
|
}
|
||||||
|
|
||||||
|
loadJS() {
|
||||||
|
try {
|
||||||
|
delete require.cache[require.resolve(this.filePath)];
|
||||||
|
const module = require(this.filePath);
|
||||||
|
if (typeof module !== "object") {
|
||||||
|
console.warn("[LiveJSON] JS-Datei exportiert kein Objekt:", this.filePath);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return this.deepCopy(module);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[LiveJSON] Fehler beim Laden der JS-Datei ${this.filePath}:`, err.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadJSON() {
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(this.filePath, 'utf-8');
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[LiveJSON] Fehler beim Laden von ${this.filePath}:`, err.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFile() {
|
||||||
|
if (this.fileType === "js") return this.loadJS();
|
||||||
|
return this.loadJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveToFile() {
|
||||||
|
try {
|
||||||
|
if (this.fileType === 'json') {
|
||||||
|
fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2), 'utf-8');
|
||||||
|
} else if (this.fileType === 'js' && this.autoSaveJs) {
|
||||||
|
const content = 'module.exports = ' + JSON.stringify(this.data, null, 2);
|
||||||
|
fs.writeFileSync(this.filePath, content, 'utf-8');
|
||||||
|
}
|
||||||
|
return { levelId: 0, pluginName: null, message: 'Änderungen erfolgreich gespeichert' };
|
||||||
|
} catch (err) {
|
||||||
|
return { levelId: 4, pluginName: null, message: `Fehler beim Schreiben in Datei: ${err.message}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createProxy(obj) {
|
||||||
|
const self = this;
|
||||||
|
return new Proxy(obj, {
|
||||||
|
get(target, prop) {
|
||||||
|
const value = target[prop];
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return self.createProxy(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
set(target, prop, value) {
|
||||||
|
const oldValue = self.deepCopy(target);
|
||||||
|
const result = Reflect.set(target, prop, value);
|
||||||
|
const newValue = self.deepCopy(target);
|
||||||
|
self.pushHistory(oldValue, newValue);
|
||||||
|
self.saveToFile();
|
||||||
|
self.triggerCallbacks(oldValue, newValue);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
deleteProperty(target, prop) {
|
||||||
|
const oldValue = self.deepCopy(target);
|
||||||
|
const result = Reflect.deleteProperty(target, prop);
|
||||||
|
const newValue = self.deepCopy(target);
|
||||||
|
self.pushHistory(oldValue, newValue);
|
||||||
|
self.saveToFile();
|
||||||
|
self.triggerCallbacks(oldValue, newValue);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reload() {
|
||||||
|
setTimeout(() => {
|
||||||
|
const oldData = this.deepCopy(this.data);
|
||||||
|
const newData = this.loadFile();
|
||||||
|
if (newData) {
|
||||||
|
this.mergeObjects(this.data, newData);
|
||||||
|
this.pushHistory(oldData, this.deepCopy(this.data));
|
||||||
|
// Trigger Callback jetzt mit absPath
|
||||||
|
this.triggerCallbacks(oldData, this.data, this.filePath);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
return { levelId: 0, message: 'Reload initiiert' };
|
||||||
|
}
|
||||||
|
|
||||||
|
deepCopy(obj) {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
pushHistory(oldState, newState) {
|
||||||
|
const entry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
oldState,
|
||||||
|
newState
|
||||||
|
};
|
||||||
|
this.history.push(entry);
|
||||||
|
if (this.history.length > this.historyLimit) this.history.shift();
|
||||||
|
this.redoStack = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
undo() {
|
||||||
|
if (this.history.length < 2) return { levelId: 4, message: 'Nichts zum Rückgängigmachen' };
|
||||||
|
const last = this.history.pop();
|
||||||
|
this.redoStack.push(last);
|
||||||
|
const prev = this.history[this.history.length - 1];
|
||||||
|
this.mergeObjects(this.data, prev.newState, true);
|
||||||
|
this.saveToFile();
|
||||||
|
this.triggerCallbacks(last.newState, prev.newState);
|
||||||
|
return { levelId: 0, message: 'Undo erfolgreich', state: prev.newState };
|
||||||
|
}
|
||||||
|
|
||||||
|
redo() {
|
||||||
|
if (this.redoStack.length === 0) return { levelId: 4, message: 'Nichts zum Wiederherstellen' };
|
||||||
|
const next = this.redoStack.pop();
|
||||||
|
this.history.push(next);
|
||||||
|
this.mergeObjects(this.data, next.newState, true);
|
||||||
|
this.saveToFile();
|
||||||
|
this.triggerCallbacks(next.oldState, next.newState);
|
||||||
|
return { levelId: 0, message: 'Redo erfolgreich', state: next.newState };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rekursive Diff inkl. Arrays
|
||||||
|
diff(oldObj, newObj) {
|
||||||
|
if (Array.isArray(oldObj) && Array.isArray(newObj)) return this.diffArray(oldObj, newObj);
|
||||||
|
if (this.isObject(oldObj) && this.isObject(newObj)) {
|
||||||
|
const changes = {};
|
||||||
|
const allKeys = new Set([...Object.keys(oldObj || {}), ...Object.keys(newObj || {})]);
|
||||||
|
allKeys.forEach(key => {
|
||||||
|
const oldVal = oldObj[key];
|
||||||
|
const newVal = newObj[key];
|
||||||
|
if (this.isObject(oldVal) && this.isObject(newVal)) {
|
||||||
|
const childDiff = this.diff(oldVal, newVal);
|
||||||
|
if (Object.keys(childDiff).length > 0) changes[key] = childDiff;
|
||||||
|
} else if (Array.isArray(oldVal) && Array.isArray(newVal)) {
|
||||||
|
const arrayDiff = this.diffArray(oldVal, newVal);
|
||||||
|
if (arrayDiff.length > 0) changes[key] = arrayDiff;
|
||||||
|
} else if (oldVal !== newVal) {
|
||||||
|
changes[key] = { old: oldVal, new: newVal };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
return oldObj !== newObj ? { old: oldObj, new: newObj } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diff für Arrays auf Elementebene
|
||||||
|
diffArray(oldArr, newArr) {
|
||||||
|
const maxLen = Math.max(oldArr.length, newArr.length);
|
||||||
|
const changes = [];
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
if (i >= oldArr.length) {
|
||||||
|
changes.push({ index: i, old: undefined, new: newArr[i] });
|
||||||
|
} else if (i >= newArr.length) {
|
||||||
|
changes.push({ index: i, old: oldArr[i], new: undefined });
|
||||||
|
} else if (JSON.stringify(oldArr[i]) !== JSON.stringify(newArr[i])) {
|
||||||
|
changes.push({ index: i, old: oldArr[i], new: newArr[i] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
isObject(value) {
|
||||||
|
return value && typeof value === 'object' && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge für Objekte und Arrays
|
||||||
|
mergeObjects(target, source, removeExtra = false) {
|
||||||
|
if (Array.isArray(target) && Array.isArray(source)) {
|
||||||
|
target.length = 0;
|
||||||
|
source.forEach((el) => target.push(this.deepCopy(el)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(target).forEach(key => {
|
||||||
|
if (source[key] === undefined && removeExtra) delete target[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(source).forEach(key => {
|
||||||
|
if (this.isObject(source[key])) {
|
||||||
|
if (!target[key] || !this.isObject(target[key])) target[key] = {};
|
||||||
|
this.mergeObjects(target[key], source[key], removeExtra);
|
||||||
|
} else if (Array.isArray(source[key])) {
|
||||||
|
target[key] = [];
|
||||||
|
this.mergeObjects(target[key], source[key], removeExtra);
|
||||||
|
} else {
|
||||||
|
target[key] = source[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(cb) {
|
||||||
|
if (typeof cb === 'function') this.callbacks.push(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerCallbacks(oldState, newState, absPath = null) {
|
||||||
|
const delta = this.diff(oldState, newState);
|
||||||
|
this.callbacks.forEach(cb => cb({ oldState, newState, delta, path: absPath }));
|
||||||
|
}
|
||||||
|
|
||||||
|
get live() {
|
||||||
|
return this.proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.watcher.close();
|
||||||
|
return { levelId: 0, pluginName: null, message: 'Watcher gestoppt' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
Folder: class {
|
||||||
|
constructor(folderPath, options = {}) {
|
||||||
|
if (!folderPath || typeof folderPath !== 'string') {
|
||||||
|
return {
|
||||||
|
levelId: 4,
|
||||||
|
pluginName: null,
|
||||||
|
message: "folderPath muss ein gültiger String sein"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.folderPath = path.resolve(folderPath);
|
||||||
|
this.files = {}; // relPath -> File-Instanz
|
||||||
|
this.liveProxies = {}; // relPath -> Proxy für live Zugriff
|
||||||
|
|
||||||
|
this.options = {
|
||||||
|
ignoreInitial: false,
|
||||||
|
persistent: true,
|
||||||
|
ignored: options.ignored ?? null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
this.handlers = {
|
||||||
|
onAdd: options.onAdd || (() => {}),
|
||||||
|
onChange: options.onChange || (() => {}),
|
||||||
|
onUnlink: options.onUnlink || (() => {}),
|
||||||
|
onAddDir: options.onAddDir || (() => {}),
|
||||||
|
onUnlinkDir: options.onUnlinkDir || (() => {}),
|
||||||
|
onReady: options.onReady || (() => {}),
|
||||||
|
onError: options.onError || (() => {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.watcher = watch(this.folderPath, this.options);
|
||||||
|
this.bindEvents();
|
||||||
|
return { levelId: 0, pluginName: null, message: "SmartFolderWatcher mit History erfolgreich initialisiert" };
|
||||||
|
} catch (err) {
|
||||||
|
return { levelId: 4, pluginName: null, message: `Watcher konnte nicht gestartet werden: ${err.message}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datei initial laden / als File-Instanz tracken
|
||||||
|
loadTrackedFile(absPath) {
|
||||||
|
const rel = path.relative(this.folderPath, absPath);
|
||||||
|
const ext = path.extname(absPath).toLowerCase();
|
||||||
|
const fileType = ext === '.js' ? 'js' : 'json';
|
||||||
|
|
||||||
|
// File-Instanz erstellen, falls nicht existiert
|
||||||
|
if (!this.files[rel]) {
|
||||||
|
const { File } = module.exports;
|
||||||
|
const fileInstance = new File(absPath, { fileType, autoCreate: true, autoSaveJs: true });
|
||||||
|
this.files[rel] = fileInstance;
|
||||||
|
this.liveProxies[rel] = fileInstance.live;
|
||||||
|
} else {
|
||||||
|
// reload falls schon bekannt
|
||||||
|
this.files[rel].reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.files[rel];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chokidar Events
|
||||||
|
bindEvents() {
|
||||||
|
this.watcher
|
||||||
|
.on("add", (file) => {
|
||||||
|
const rel = path.relative(this.folderPath, file);
|
||||||
|
const fileInstance = this.loadTrackedFile(file);
|
||||||
|
this.handlers.onAdd(rel, fileInstance.data, file); // rel, Inhalt, absPath
|
||||||
|
})
|
||||||
|
.on("change", (file) => {
|
||||||
|
const rel = path.relative(this.folderPath, file);
|
||||||
|
const fileInstance = this.loadTrackedFile(file);
|
||||||
|
this.handlers.onChange(rel, fileInstance.data, file);
|
||||||
|
})
|
||||||
|
.on("unlink", (file) => {
|
||||||
|
const rel = path.relative(this.folderPath, file);
|
||||||
|
delete this.liveProxies[rel];
|
||||||
|
delete this.files[rel];
|
||||||
|
this.handlers.onUnlink(rel, file);
|
||||||
|
})
|
||||||
|
.on("addDir", (dir) => {
|
||||||
|
const rel = path.relative(this.folderPath, dir);
|
||||||
|
this.handlers.onAddDir(rel, dir);
|
||||||
|
})
|
||||||
|
.on("unlinkDir", (dir) => {
|
||||||
|
const rel = path.relative(this.folderPath, dir);
|
||||||
|
this.handlers.onUnlinkDir(rel, dir);
|
||||||
|
})
|
||||||
|
.on("ready", () => {
|
||||||
|
this.handlers.onReady(this.liveProxies);
|
||||||
|
})
|
||||||
|
.on("error", (err) => {
|
||||||
|
this.handlers.onError(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zugriff auf alle live Proxies
|
||||||
|
get live() {
|
||||||
|
return this.liveProxies;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo / Redo auf Dateiebene
|
||||||
|
undoFile(relPath) {
|
||||||
|
if (!this.files[relPath]) return { levelId: 4, message: 'Datei nicht gefunden' };
|
||||||
|
return this.files[relPath].undo();
|
||||||
|
}
|
||||||
|
|
||||||
|
redoFile(relPath) {
|
||||||
|
if (!this.files[relPath]) return { levelId: 4, message: 'Datei nicht gefunden' };
|
||||||
|
return this.files[relPath].redo();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.watcher.close();
|
||||||
|
// close alle File-Watcher
|
||||||
|
Object.values(this.files).forEach(f => f.close());
|
||||||
|
return { levelId: 0, pluginName: null, message: "FolderWatcher gestoppt" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
302
src/services/identityManager.js
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
|
class IdentityManager {
|
||||||
|
constructor(adManager, AuthenticationModel) {
|
||||||
|
this.ad = adManager || null;
|
||||||
|
this.Authentication = AuthenticationModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* REQUIRED FIELDS (MANUAL USER)
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
REQUIRED_FIELDS = [
|
||||||
|
'sAMAccountName',
|
||||||
|
'mail',
|
||||||
|
'givenName',
|
||||||
|
'sn',
|
||||||
|
'password'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* VALIDATE MANUAL USER
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
validateManualUser(user) {
|
||||||
|
const missing = [];
|
||||||
|
|
||||||
|
for (const field of this.REQUIRED_FIELDS) {
|
||||||
|
if (
|
||||||
|
user[field] === undefined ||
|
||||||
|
user[field] === null ||
|
||||||
|
user[field] === ''
|
||||||
|
) {
|
||||||
|
missing.push(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.length) {
|
||||||
|
throw new Error(
|
||||||
|
`Fehlende Pflichtfelder: ${missing.join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔍 Optional: einfache Zusatzvalidierungen
|
||||||
|
if (user.mail && !user.mail.includes('@')) {
|
||||||
|
throw new Error('Ungültige E-Mail-Adresse');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.password && user.password.length < 6) {
|
||||||
|
throw new Error('Passwort muss mindestens 6 Zeichen lang sein');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* FIXED MAPPING (AD → Authentication)
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
mapAdObject(obj) {
|
||||||
|
if (!obj || !obj.objectGUID) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
ObjectGUID: obj.objectGUID,
|
||||||
|
sAMAccountName: obj.sAMAccountName || obj.cn || null,
|
||||||
|
mail: obj.mail || null,
|
||||||
|
givenName: obj.givenName || null,
|
||||||
|
sn: obj.sn || null,
|
||||||
|
employeeID: obj.employeeID || null,
|
||||||
|
title: obj.title || null,
|
||||||
|
department: obj.department || null,
|
||||||
|
streetAddress: obj.streetAddress || null,
|
||||||
|
userAccountControl_ID: obj.userAccountControl || null,
|
||||||
|
authenticationType_ID: 1,
|
||||||
|
telephoneNumber: obj.telephoneNumber || null,
|
||||||
|
physicalDeliveryOfficeName: obj.physicalDeliveryOfficeName || null,
|
||||||
|
distinguishedName: obj.dn || null,
|
||||||
|
password: null,
|
||||||
|
refreshtoken: null,
|
||||||
|
active: true,
|
||||||
|
online: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* DEDUP (wie SQL UNION)
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
deduplicateByGUID(items) {
|
||||||
|
const map = new Map();
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (!item?.ObjectGUID) continue;
|
||||||
|
map.set(item.ObjectGUID, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(map.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* TABLE CHECK / CREATE
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
async ensureTable() {
|
||||||
|
const qi = this.Authentication.sequelize.getQueryInterface();
|
||||||
|
const tables = await qi.showAllTables();
|
||||||
|
|
||||||
|
const exists = tables.includes('Authentication');
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
await this.Authentication.sync();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* CORE SYNC (INTELLIGENT)
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
async syncFromAD() {
|
||||||
|
if (!this.ad) {
|
||||||
|
throw new Error('AD nicht konfiguriert');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [users, groups, computers] = await Promise.all([
|
||||||
|
this.ad.findUsers('*'),
|
||||||
|
this.ad.findGroups('*'),
|
||||||
|
this.ad.getComputers()
|
||||||
|
]);
|
||||||
|
|
||||||
|
const mapped = this.deduplicateByGUID([
|
||||||
|
...users.map(u => this.mapAdObject(u)),
|
||||||
|
...groups.map(g => this.mapAdObject(g)),
|
||||||
|
...computers.map(c => this.mapAdObject(c))
|
||||||
|
].filter(Boolean));
|
||||||
|
|
||||||
|
if (!mapped.length) {
|
||||||
|
return { total: 0, deactivated: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.Authentication.bulkCreate(mapped, {
|
||||||
|
updateOnDuplicate: [
|
||||||
|
'mail',
|
||||||
|
'givenName',
|
||||||
|
'sn',
|
||||||
|
'employeeID',
|
||||||
|
'title',
|
||||||
|
'department',
|
||||||
|
'streetAddress',
|
||||||
|
'userAccountControl_ID',
|
||||||
|
'telephoneNumber',
|
||||||
|
'physicalDeliveryOfficeName',
|
||||||
|
'distinguishedName',
|
||||||
|
'active'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const existing = await this.Authentication.findAll({
|
||||||
|
where: { authenticationType_ID: 1 },
|
||||||
|
attributes: ['ObjectGUID']
|
||||||
|
});
|
||||||
|
|
||||||
|
const adGuids = new Set(mapped.map(u => u.ObjectGUID));
|
||||||
|
|
||||||
|
const toDeactivate = existing
|
||||||
|
.filter(e => !adGuids.has(e.ObjectGUID))
|
||||||
|
.map(e => e.ObjectGUID);
|
||||||
|
|
||||||
|
if (toDeactivate.length) {
|
||||||
|
await this.Authentication.update(
|
||||||
|
{ active: false },
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
ObjectGUID: toDeactivate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: mapped.length,
|
||||||
|
deactivated: toDeactivate.length,
|
||||||
|
adGuids: Array.from(adGuids)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* OPTIONAL: HARD DELETE
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
async removeDeletedADObjects(adGuids) {
|
||||||
|
return this.Authentication.destroy({
|
||||||
|
where: {
|
||||||
|
authenticationType_ID: 1,
|
||||||
|
ObjectGUID: {
|
||||||
|
[Op.notIn]: adGuids
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* RECREATE
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
async recreateAuthentications(hardReset = false) {
|
||||||
|
let message = '';
|
||||||
|
|
||||||
|
const exists = await this.ensureTable();
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
message = 'Tabelle wurde neu erstellt ';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.syncFromAD();
|
||||||
|
|
||||||
|
message += `Sync abgeschlossen (${result.total} Objekte)`;
|
||||||
|
|
||||||
|
if (result.deactivated) {
|
||||||
|
message += `, ${result.deactivated} deaktiviert`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hardReset) {
|
||||||
|
const deleted = await this.removeDeletedADObjects(result.adGuids);
|
||||||
|
message += `, ${deleted} gelöscht`;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
message += 'Fehler: ' + err.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* MANUAL USER (MIT VALIDATION)
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
async createManualUser(user) {
|
||||||
|
this.validateManualUser(user);
|
||||||
|
|
||||||
|
return this.Authentication.create({
|
||||||
|
...user,
|
||||||
|
authenticationType_ID: 2,
|
||||||
|
active: true,
|
||||||
|
online: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* MANUAL BULK (MIT VALIDATION)
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
async createManualUsers(users) {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
users.forEach((user, index) => {
|
||||||
|
try {
|
||||||
|
this.validateManualUser(user);
|
||||||
|
} catch (err) {
|
||||||
|
errors.push(`User ${index}: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
throw new Error(errors.join(' | '));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.Authentication.bulkCreate(
|
||||||
|
users.map(user => ({
|
||||||
|
...user,
|
||||||
|
authenticationType_ID: 2,
|
||||||
|
active: true,
|
||||||
|
online: false
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* GET USER
|
||||||
|
* -----------------------------------------------------
|
||||||
|
*/
|
||||||
|
async getUser(username) {
|
||||||
|
return this.Authentication.findOne({
|
||||||
|
where: { sAMAccountName: username }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = IdentityManager;
|
||||||
137
src/services/notifyTrayManager.js
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
class notifyTrayManager {
|
||||||
|
/**
|
||||||
|
* @param {object} models - { UserNotificationObjects, UserNotifications }
|
||||||
|
*/
|
||||||
|
constructor(notifyTrayModel, notifyTrayView, notifyTrayObject) {
|
||||||
|
this.objects = notifyTrayObject;
|
||||||
|
this.view = notifyTrayView;
|
||||||
|
this.notifications = notifyTrayModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------
|
||||||
|
// 1. Notification Object erstellen oder updaten
|
||||||
|
//--------------------------------------------
|
||||||
|
async upsertObject({ id = null, pluginName, message, json, actionRequired = false, expiresAt = null }) {
|
||||||
|
if (id) {
|
||||||
|
// ID existiert → upsert
|
||||||
|
return this.objects.upsert({
|
||||||
|
ID: id,
|
||||||
|
PluginName: pluginName,
|
||||||
|
Message: message,
|
||||||
|
JSON: json ? JSON.stringify(json) : null,
|
||||||
|
ActionRequired: actionRequired,
|
||||||
|
CreatedAt: new Date(),
|
||||||
|
ExpiresAt: expiresAt
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// ID null → neue Zeile einfügen, Auto-Increment nutzen
|
||||||
|
const obj = await this.objects.create({
|
||||||
|
PluginName: pluginName,
|
||||||
|
Message: message,
|
||||||
|
JSON: json ? JSON.stringify(json) : null,
|
||||||
|
ActionRequired: actionRequired,
|
||||||
|
CreatedAt: new Date(),
|
||||||
|
ExpiresAt: expiresAt
|
||||||
|
});
|
||||||
|
|
||||||
|
// zurückgeben inklusive der generierten ID
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------
|
||||||
|
// 2. User benachrichtigen (insert)
|
||||||
|
//--------------------------------------------
|
||||||
|
async notifyUsers({ objectId, objectGuids = [] }) {
|
||||||
|
|
||||||
|
if (!objectGuids.length) return;
|
||||||
|
const notificationsToCreate = objectGuids.map(guid => ({
|
||||||
|
ObjectGUID: guid,
|
||||||
|
NotifyTrayObject_ID: objectId,
|
||||||
|
}));
|
||||||
|
return this.notifications.bulkCreate(notificationsToCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------
|
||||||
|
// 3. Offene Notifications abfragen
|
||||||
|
//--------------------------------------------
|
||||||
|
async getOpenNotifications(objectGuid) {
|
||||||
|
return await this.view.findAll({
|
||||||
|
where: {
|
||||||
|
ObjectGUID: objectGuid,
|
||||||
|
SeenAt: null
|
||||||
|
},
|
||||||
|
// include: [{
|
||||||
|
// model: this.objects,
|
||||||
|
// as: 'NotificationObject',
|
||||||
|
// required: true, // join zwingend
|
||||||
|
// where: {
|
||||||
|
// [this.objects.sequelize.Op.Or]: [
|
||||||
|
// { ExpiresAt: null },
|
||||||
|
// { ExpiresAt: { [this.objects.sequelize.Op.gt]: new Date() } }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// }],
|
||||||
|
order: [[ 'SeenAt', 'ASC']]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------
|
||||||
|
// 4. Einzelne Notification als gesehen markieren
|
||||||
|
//--------------------------------------------
|
||||||
|
async markAsSeen(objectGuid, notificationId, value) {
|
||||||
|
return this.notifications.update(
|
||||||
|
{ SeenAt: value ? new Date() : null },
|
||||||
|
{ where: { ID: notificationId, ObjectGUID: objectGuid } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------
|
||||||
|
// 5. Alle Notifications eines Users als gesehen markieren
|
||||||
|
//--------------------------------------------
|
||||||
|
async markAllAsSeen(objectGuid, value) {
|
||||||
|
return this.notifications.update(
|
||||||
|
{ SeenAt: value ? new Date() : null },
|
||||||
|
{ where: { ObjectGUID: objectGuid, SeenAt: null } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------
|
||||||
|
// 6. Komplett-Flow: Object upserten + User benachrichtigen
|
||||||
|
//--------------------------------------------
|
||||||
|
async createAndNotify({ objectId = null, pluginName, json, actionRequired, message, objectGuids, expiresAt }) {
|
||||||
|
if(objectGuids === null || !objectGuids.length) {
|
||||||
|
throw 'There is no user to notify for tray';
|
||||||
|
}
|
||||||
|
let obj;
|
||||||
|
if (objectId) {
|
||||||
|
obj = await this.upsertObject({
|
||||||
|
id: objectId,
|
||||||
|
pluginName,
|
||||||
|
message,
|
||||||
|
json,
|
||||||
|
actionRequired,
|
||||||
|
expiresAt: Date.now()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
obj = await this.upsertObject({
|
||||||
|
id: null,
|
||||||
|
pluginName,
|
||||||
|
message,
|
||||||
|
json,
|
||||||
|
actionRequired,
|
||||||
|
expiresAt: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-Increment ID korrekt für Notifications nutzen
|
||||||
|
const finalObjectId = obj.ID || obj.id; // Sequelize gibt je nach DB dialect mal 'ID' oder 'id'
|
||||||
|
|
||||||
|
return await this.notifyUsers({
|
||||||
|
objectId: finalObjectId,
|
||||||
|
objectGuids
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = notifyTrayManager;
|
||||||
497
src/services/pluginManager.js
Normal file
@@ -0,0 +1,497 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const fse = require('fs-extra');
|
||||||
|
const expressHandlebars = require('express-handlebars');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const { File: HotReload } = require(`@services/hotReload.js`);
|
||||||
|
|
||||||
|
class PluginManager {
|
||||||
|
constructor(app, pluginModel, pluginBasePath, filePermissions, service) {
|
||||||
|
this.app = app;
|
||||||
|
this.Plugin = pluginModel;
|
||||||
|
this.pluginBasePath = pluginBasePath;
|
||||||
|
this.filePermissions = filePermissions;
|
||||||
|
this.service = service;
|
||||||
|
|
||||||
|
this.plugins = new Map();
|
||||||
|
|
||||||
|
// FIX: metadata jetzt Map (aber Struktur bleibt gleich)
|
||||||
|
this.metadata = new Map();
|
||||||
|
|
||||||
|
this.hbsInstance = expressHandlebars.create({
|
||||||
|
extname: '.hbs',
|
||||||
|
defaultLayout: 'main',
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!fs.existsSync(this.pluginBasePath)) {
|
||||||
|
fs.mkdirSync(this.pluginBasePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadAll() {
|
||||||
|
if (!fs.existsSync(this.pluginBasePath)) {
|
||||||
|
return [{ levelId: 4, message: `Plugin-Pfad existiert nicht: ${this.pluginBasePath}` }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginDirs = fs.readdirSync(this.pluginBasePath)
|
||||||
|
.filter(f => fs.lstatSync(path.join(this.pluginBasePath, f)).isDirectory());
|
||||||
|
|
||||||
|
const loadPromises = pluginDirs.map(dir => this.load(dir));
|
||||||
|
return await Promise.all(loadPromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(name, withActivate = false) {
|
||||||
|
const pluginPath = path.join(this.pluginBasePath, name);
|
||||||
|
const metadataPath = path.join(pluginPath, 'plugin.json');
|
||||||
|
|
||||||
|
const Plugin = require(pluginPath);
|
||||||
|
const instance = new Plugin(pluginPath, this.app, this.service);
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// FIX: HotReload pro Plugin korrekt speichern
|
||||||
|
const meta = new HotReload(metadataPath, {
|
||||||
|
historyLimit: 50,
|
||||||
|
autoCreate: true
|
||||||
|
});
|
||||||
|
|
||||||
|
this.metadata.set(name, meta);
|
||||||
|
|
||||||
|
if (Object.keys(meta.live).length === 0) {
|
||||||
|
meta.live = this.__pluginTemplate(name);
|
||||||
|
meta.saveToFile();
|
||||||
|
this.__setPermissions(pluginPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
const routesPath = path.join(pluginPath, 'routes.js');
|
||||||
|
if (fs.existsSync(routesPath)) {
|
||||||
|
const routes = require(routesPath);
|
||||||
|
routes(this.app, pluginPath, meta.live, this.service);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sockets
|
||||||
|
const socketsPath = path.join(pluginPath, 'sockets.js');
|
||||||
|
if (fs.existsSync(socketsPath)) {
|
||||||
|
const sockets = require(socketsPath);
|
||||||
|
sockets(this.app, pluginPath, meta.live, this.service);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.app.use(`/${name}`, require('express').static(path.join(pluginPath, 'public')));
|
||||||
|
|
||||||
|
const viewsDir = path.join(pluginPath, 'views');
|
||||||
|
|
||||||
|
this.app.set("views", [
|
||||||
|
...(Array.isArray(this.app.get("views")) ? this.app.get("views") : [this.app.get("views")]),
|
||||||
|
path.join(this.pluginBasePath)
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.registerPartialsRecursive(viewsDir, `plugins/${name}`);
|
||||||
|
|
||||||
|
await this.Plugin.upsert({
|
||||||
|
Name: name,
|
||||||
|
Active: withActivate ? true : (await this.Plugin.findOne({ where: { Name: meta.live.name }})).Active,
|
||||||
|
Version: meta.live.version
|
||||||
|
});
|
||||||
|
|
||||||
|
const static_meta = this.service.get('fileSystemManager').loadJSON(metadataPath);
|
||||||
|
|
||||||
|
this.plugins.set(name, {
|
||||||
|
...static_meta,
|
||||||
|
pluginPath,
|
||||||
|
viewPath: viewsDir,
|
||||||
|
metadataPath
|
||||||
|
});
|
||||||
|
|
||||||
|
meta.live.active = withActivate ? true : (await this.Plugin.findOne({ where: { Name: meta.live.name }})).Active;
|
||||||
|
|
||||||
|
result = {
|
||||||
|
status: 'load',
|
||||||
|
pluginName: name,
|
||||||
|
metadata: meta.live,
|
||||||
|
levelId: 0,
|
||||||
|
message: `Plugin ${name} geladen`
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
return {
|
||||||
|
status: 'load',
|
||||||
|
pluginName: name,
|
||||||
|
levelId: 4,
|
||||||
|
message: [err.message, err]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unload(name) {
|
||||||
|
let result;
|
||||||
|
const pluginPath = path.join(this.pluginBasePath, name);
|
||||||
|
const metadataPath = path.join(pluginPath, 'plugin.json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.plugins.has(name)) {
|
||||||
|
return {
|
||||||
|
status: 'unload',
|
||||||
|
pluginName: name,
|
||||||
|
levelId: 4,
|
||||||
|
message: 'Plugin nicht vorhanden'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIX: korrektes metadata handling
|
||||||
|
const meta = this.metadata.get(name);
|
||||||
|
|
||||||
|
if (meta) {
|
||||||
|
meta.live.active = false;
|
||||||
|
meta.saveToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.Plugin.update({ Active: false }, { where: { Name: name } });
|
||||||
|
|
||||||
|
result = {
|
||||||
|
status: 'unload',
|
||||||
|
pluginName: name,
|
||||||
|
levelId: 0,
|
||||||
|
message: `Plugin ${name} entladen`
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
status: 'unload',
|
||||||
|
pluginName: name,
|
||||||
|
levelId: 4,
|
||||||
|
message: [err.message, err]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reload(name) {
|
||||||
|
const unloadResult = await this.unload(name);
|
||||||
|
if (unloadResult.levelId === 4) return unloadResult;
|
||||||
|
|
||||||
|
delete require.cache[require.resolve(path.join(this.pluginBasePath, name))];
|
||||||
|
|
||||||
|
return await this.load(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(name) {
|
||||||
|
const pluginPath = path.join(this.pluginBasePath, name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.unload(name);
|
||||||
|
|
||||||
|
this.plugins.delete(name);
|
||||||
|
await this.Plugin.destroy({ where: { Name: name } });
|
||||||
|
|
||||||
|
if (fs.existsSync(pluginPath)) {
|
||||||
|
await fse.remove(pluginPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'delete',
|
||||||
|
pluginName: name,
|
||||||
|
levelId: 0,
|
||||||
|
message: `Plugin ${name} gelöscht`
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
status: 'delete',
|
||||||
|
pluginName: name,
|
||||||
|
levelId: 4,
|
||||||
|
message: [err.message, err]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(name, options = {}) {
|
||||||
|
const pluginPath = path.join(this.pluginBasePath, name);
|
||||||
|
|
||||||
|
if (fs.existsSync(pluginPath)) {
|
||||||
|
return { levelId: 4, pluginName: name, message: `Plugin existiert bereits` };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const folders = [
|
||||||
|
'views',
|
||||||
|
'views/children',
|
||||||
|
'public/javascript',
|
||||||
|
'public/styles',
|
||||||
|
'public/helpers',
|
||||||
|
'public/images',
|
||||||
|
'public/others',
|
||||||
|
'docs'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const folder of folders) {
|
||||||
|
await fse.ensureDir(path.join(pluginPath, folder));
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = {
|
||||||
|
'plugin.json': this.__pluginTemplate(name),
|
||||||
|
|
||||||
|
'index.js':
|
||||||
|
`module.exports = class Plugin {
|
||||||
|
constructor(pluginPath, app, service) {
|
||||||
|
this.pluginPath = pluginPath;
|
||||||
|
this.app = app;
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
|
||||||
|
'routes.js':
|
||||||
|
`module.exports = async (app, pluginPath, metadata, service) => {};`,
|
||||||
|
|
||||||
|
'sockets.js':
|
||||||
|
`module.exports = (app, socketManager, pluginPath, metadata, eventManager) => {};`,
|
||||||
|
|
||||||
|
'docs/help.html':
|
||||||
|
`<h1>Hilfedatei für ${name}</h1><p>${options.description || 'Beschreibung hier einfügen'}</p>`,
|
||||||
|
|
||||||
|
'views/index.hbs':
|
||||||
|
`<div>{{plugin.name}}</div>`
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [file, content] of Object.entries(files)) {
|
||||||
|
await fse.outputFile(path.join(pluginPath, file), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__setPermissions(pluginPath);
|
||||||
|
|
||||||
|
await this.Plugin.upsert({
|
||||||
|
Name: name,
|
||||||
|
Active: false,
|
||||||
|
Version: options.version || '1.0.0'
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'create',
|
||||||
|
levelId: 0,
|
||||||
|
pluginName: name,
|
||||||
|
message: `Plugin ${name} erstellt`
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
status: 'create',
|
||||||
|
levelId: 4,
|
||||||
|
pluginName: name,
|
||||||
|
message: err.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__pluginTemplate(name, options = { }) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
description: options.description || 'Beschreibung hier einfügen',
|
||||||
|
version: options.version || `1.${new Date().getFullYear().toString().slice(-2)}.${new Date().getMonth() + 1}.${new Date().getDate()}`,
|
||||||
|
menu: {
|
||||||
|
label: name,
|
||||||
|
items:[
|
||||||
|
{
|
||||||
|
label: name,
|
||||||
|
view: "index",
|
||||||
|
defaultSize: { width: '800px', height: '600px' },
|
||||||
|
icon: "../../images/app.png",
|
||||||
|
permissions: ["*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
config: options.config || {},
|
||||||
|
active: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__setPermissions(pluginPath) {
|
||||||
|
if (!this.filePermissions.user && !this.filePermissions.group) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let uid, gid;
|
||||||
|
|
||||||
|
if (this.filePermissions.user) {
|
||||||
|
uid = parseInt(execSync(`id -u ${this.filePermissions.user}`).toString().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.filePermissions.group) {
|
||||||
|
gid = parseInt(execSync(`getent group ${this.filePermissions.group} | cut -d: -f3`).toString().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
const apply = dir => {
|
||||||
|
const entries = fs.readdirSync(dir);
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const full = path.join(dir, entry);
|
||||||
|
const stat = fs.lstatSync(full);
|
||||||
|
|
||||||
|
fs.chownSync(full, uid, gid);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) apply(full);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
apply(pluginPath);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(name, updates = {}) {
|
||||||
|
const meta = this.metadata.get(name);
|
||||||
|
|
||||||
|
if (!meta) {
|
||||||
|
return { status: 'update', levelId: 4, message: 'Plugin nicht geladen' };
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(meta.live, updates);
|
||||||
|
|
||||||
|
meta.saveToFile?.();
|
||||||
|
return {
|
||||||
|
status: 'update',
|
||||||
|
pluginName: name,
|
||||||
|
levelId: 0,
|
||||||
|
metadata: meta.live,
|
||||||
|
message: `Plugin aktualisiert ${Object.keys(updates)[0]}: ${Object.values(updates)[0]}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async rename(oldName, newName) {
|
||||||
|
const oldPath = path.join(this.pluginBasePath, oldName);
|
||||||
|
const newPath = path.join(this.pluginBasePath, newName);
|
||||||
|
const newMetadataPath = path.join(newPath, 'plugin.json');
|
||||||
|
const routesPath = path.join(newPath, 'routes.js');
|
||||||
|
|
||||||
|
const updateRoutesFile = async (filePath, oldName, newName) => {
|
||||||
|
let content = await fse.readFile(filePath, 'utf-8');
|
||||||
|
|
||||||
|
// ersetzt /window/OLDNAME/... → /window/NEWNAME/...
|
||||||
|
const regex = new RegExp(`(app\\.(get|post)\\(['"\`]\\/window\\/)${oldName}(\\/[^'"\`]*['"\`])`, 'g');
|
||||||
|
|
||||||
|
content = content.replace(regex, `$1${newName}$3`);
|
||||||
|
|
||||||
|
await fse.writeFile(filePath, content, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 🔹 Checks
|
||||||
|
if (!this.plugins.has(oldName)) {
|
||||||
|
return {
|
||||||
|
status: 'rename',
|
||||||
|
levelId: 4,
|
||||||
|
message: 'Plugin nicht gefunden'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(newPath)) {
|
||||||
|
return {
|
||||||
|
status: 'rename',
|
||||||
|
levelId: 4,
|
||||||
|
message: 'Neuer Name existiert bereits'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 1. copy
|
||||||
|
await fse.copy(oldPath, newPath);
|
||||||
|
|
||||||
|
// 🔹 2. plugin.json fix
|
||||||
|
const metaRaw = this.service.get('fileSystemManager').loadJSON(newMetadataPath);
|
||||||
|
metaRaw.name = newName;
|
||||||
|
metaRaw.menu.label = newName;
|
||||||
|
await fse.writeJSON(newMetadataPath, metaRaw, { spaces: 2 });
|
||||||
|
|
||||||
|
// 🔹 3. routes fix (falls vorhanden)
|
||||||
|
if (fs.existsSync(routesPath)) {
|
||||||
|
await updateRoutesFile(routesPath, oldName, newName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 4. ALTES Plugin vorher unloaden
|
||||||
|
await this.unload(oldName);
|
||||||
|
|
||||||
|
// 🔹 5. metadata cleanup
|
||||||
|
this.metadata.delete(oldName);
|
||||||
|
|
||||||
|
// 🔹 6. DB update
|
||||||
|
await this.Plugin.update(
|
||||||
|
{ Name: newName },
|
||||||
|
{ where: { Name: oldName } }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔥 7. kurzer delay → verhindert ENOENT durch watcher
|
||||||
|
await new Promise(res => setTimeout(res, 100));
|
||||||
|
|
||||||
|
// 🔹 8. neues Plugin laden
|
||||||
|
await this.load(newName, metaRaw.active);
|
||||||
|
|
||||||
|
// 🔹 9. alten Ordner löschen
|
||||||
|
await fse.remove(oldPath);
|
||||||
|
|
||||||
|
// 🔹 10. altes Plugin löschen
|
||||||
|
this.plugins.delete(oldName);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'rename',
|
||||||
|
pluginName: newName,
|
||||||
|
levelId: 0,
|
||||||
|
message: `Plugin ${oldName} → ${newName} umbenannt`
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
// 🔹 Rollback: neuen Ordner entfernen
|
||||||
|
if (fs.existsSync(newPath)) {
|
||||||
|
await fse.remove(newPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'rename',
|
||||||
|
pluginName: oldName,
|
||||||
|
levelId: 4,
|
||||||
|
message: [err.message, err]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus() {
|
||||||
|
return Array.from(this.plugins.entries()).map(([name, plugin]) => {
|
||||||
|
const meta = this.metadata.get(name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
...plugin,
|
||||||
|
...(meta ? meta.live : {})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
compileHbsTemplate(template) {
|
||||||
|
return this.hbsInstance.handlebars.compile(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPartialsRecursive(dir, prefix = '') {
|
||||||
|
if (!fs.existsSync(dir)) return;
|
||||||
|
|
||||||
|
const files = fs.readdirSync(dir);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const fullPath = path.join(dir, file);
|
||||||
|
const stat = fs.lstatSync(fullPath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
this.registerPartialsRecursive(fullPath, path.join(prefix, file));
|
||||||
|
} else if (file.endsWith('.hbs')) {
|
||||||
|
const name = path.join(prefix, path.basename(file, '.hbs')).replace(/\\/g, '/');
|
||||||
|
const template = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
this.hbsInstance.handlebars.registerPartial(name, template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PluginManager;
|
||||||
85
src/services/renderWindow.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
const { service } = require('@root/server.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
renderWindow: async function(app, view, data = {}, extraData = {}, res) {
|
||||||
|
const name = res.req.body.name;
|
||||||
|
const label = res.req.body.viewLabel;
|
||||||
|
try {
|
||||||
|
const plugin = app.locals.startMenuItems.find(plugin => plugin.name == name);
|
||||||
|
const windowData = plugin.menu.items.find(item => item.label == label);
|
||||||
|
|
||||||
|
// Alle Daten zusammenführen
|
||||||
|
const templateData = {
|
||||||
|
...{ appname: plugin.menu.label, label: label, section: plugin.section },
|
||||||
|
...windowData,
|
||||||
|
// ...data,
|
||||||
|
...extraData
|
||||||
|
};
|
||||||
|
// console.log(templateData)
|
||||||
|
|
||||||
|
// Zuerst Plugin-View rendern
|
||||||
|
app.render(view, templateData, (err, contentHtml) => {
|
||||||
|
if (err) {
|
||||||
|
service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, name, err);
|
||||||
|
return res.status(500).send(err.message);
|
||||||
|
}
|
||||||
|
// Dann Window-Partial rendern
|
||||||
|
app.render('partials/window', {
|
||||||
|
layout: false,
|
||||||
|
contentHtml,
|
||||||
|
...templateData
|
||||||
|
}, (err, html) => {
|
||||||
|
if (err) {
|
||||||
|
service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, name, err);
|
||||||
|
return res.status(500).send(err.message);
|
||||||
|
}
|
||||||
|
res.status(200).send(html);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, name, err);
|
||||||
|
res.status(500).send(err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderView: async function(app, view, data = {}, res) {
|
||||||
|
const payload = res.req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// console.log(app.locals.startMenuItems.find(name == ).menu.items.find(item => ))
|
||||||
|
// Alle Daten zusammenführen
|
||||||
|
const templateData = {
|
||||||
|
...payload,
|
||||||
|
...{ appname: payload.name, section: 'view' },
|
||||||
|
...{ name: payload.name, view: payload.view, viewLabel: payload.viewLabel},
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
console.log(templateData)
|
||||||
|
// Zuerst Plugin-View rendern
|
||||||
|
app.render(view, templateData, (err, contentHtml) => {
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, payload.name, err);
|
||||||
|
return res.status(500).send(err.message);
|
||||||
|
}
|
||||||
|
// Dann Window-Partial rendern
|
||||||
|
app.render('partials/child', {
|
||||||
|
layout: false,
|
||||||
|
contentHtml,
|
||||||
|
...templateData
|
||||||
|
}, (err, html) => {
|
||||||
|
if (err) {
|
||||||
|
service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, payload.name, err);
|
||||||
|
return res.status(500).send(err.message);
|
||||||
|
}
|
||||||
|
res.status(200).send(html);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, payload.name, err);
|
||||||
|
res.status(500).send(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
336
src/services/socketManager.js
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
class SocketManager {
|
||||||
|
constructor(io) {
|
||||||
|
this.io = io;
|
||||||
|
|
||||||
|
this.namespaces = new Map();
|
||||||
|
this.clients = new Map();
|
||||||
|
|
||||||
|
this.io.on('connection', socket => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
add(namespace, exists) {
|
||||||
|
if (this.namespaces.has(namespace)) return this.namespaces.get(namespace);
|
||||||
|
|
||||||
|
const nsp = this.io.of(namespace);
|
||||||
|
const clients = new Map();
|
||||||
|
this.clients.set(namespace, clients);
|
||||||
|
|
||||||
|
nsp.on('connection', socket => {
|
||||||
|
const { objectGuid, sAMAccountName } = socket.handshake.auth || {};
|
||||||
|
if (!objectGuid) return;
|
||||||
|
|
||||||
|
clients.set(objectGuid, {
|
||||||
|
socket,
|
||||||
|
userName: sAMAccountName
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log(`${sAMAccountName} [${objectGuid}] connected to ${namespace}`);
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
clients.delete(objectGuid);
|
||||||
|
// console.log(`${sAMAccountName} [${objectGuid}] disconnected from ${namespace}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.namespaces.set(namespace, nsp);
|
||||||
|
return nsp;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async addAsync(namespace, exists) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.add(namespace, exists)
|
||||||
|
|
||||||
|
// simulierter async Init (z. B. DB)
|
||||||
|
setTimeout(resolve, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast in namespace
|
||||||
|
* @param {string} namespace - Namespace-Name
|
||||||
|
* @param {string} event - Event-Name
|
||||||
|
* @param {any} data - Event-Data
|
||||||
|
*/
|
||||||
|
broadcast(namespace, event, data) {
|
||||||
|
const nsp = this.namespaces.get(namespace);
|
||||||
|
if (nsp) nsp.emit(event, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event-Handler für einen Namespace registrieren
|
||||||
|
* @param {string} namespace - Namespace-Name
|
||||||
|
* @param {string} event - Event-Name
|
||||||
|
* @param {Function} callback - Callback mit (socket, data)
|
||||||
|
*/
|
||||||
|
// on(namespace, event, callback) {
|
||||||
|
// const nsp = this.namespaces.get(namespace);
|
||||||
|
// if (!nsp) {
|
||||||
|
// console.warn(`Namespace ${namespace} doesn't exist`);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// nsp.on('connection', (socket) => {
|
||||||
|
// socket.on(event, (data) => {
|
||||||
|
// // übergibt socket als Kontext, damit sendTo automatisch weiß, wer anfragte
|
||||||
|
// callback(socket, data, (responseEvent, responseData, targetGuid = null) => {
|
||||||
|
// this.sendTo(namespace, targetGuid, responseEvent, responseData, socket);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
on(namespace, event, callback) {
|
||||||
|
const nsp = this.namespaces.get(namespace);
|
||||||
|
if (!nsp) {
|
||||||
|
console.warn(`Namespace ${namespace} doesn't exist`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nsp.on('connection', (socket) => {
|
||||||
|
socket.on(event, (data) => callback(socket, data));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
registerClient(namespace, socket) {
|
||||||
|
const nsp = this.namespaces.get(namespace);
|
||||||
|
if (!nsp) {
|
||||||
|
console.warn(`Namespace ${namespace} does not exist`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth = socket.handshake.auth;
|
||||||
|
const objectGuid = auth.objectGuid;
|
||||||
|
const sAMAccountName = auth.sAMAccountName;
|
||||||
|
|
||||||
|
if (!objectGuid) {
|
||||||
|
console.warn('Socket has no objectGuid, cannot register');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.customId = objectGuid;
|
||||||
|
|
||||||
|
// In Map speichern
|
||||||
|
this.clients.set(objectGuid, {
|
||||||
|
userName: sAMAccountName,
|
||||||
|
socketId: socket.id,
|
||||||
|
socket: socket
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`${sAMAccountName} [${objectGuid}] registered to namespace ${namespace}`);
|
||||||
|
|
||||||
|
// Disconnect-Handler
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
this.clients.delete(objectGuid);
|
||||||
|
console.log(`${sAMAccountName} [${objectGuid}] disconnected from namespace ${namespace}`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt einen Client aus dem Namespace
|
||||||
|
* @param {string} objectGuid - GUID des Clients
|
||||||
|
* @param {string} [namespace] - optional, um Namespace-spezifisch zu loggen
|
||||||
|
*/
|
||||||
|
unregisterClient(objectGuid) {
|
||||||
|
try {
|
||||||
|
const client = this.clients.get(objectGuid);
|
||||||
|
if (!client) return;
|
||||||
|
|
||||||
|
// Optional: Socket trennen
|
||||||
|
client.socket.disconnect();
|
||||||
|
|
||||||
|
this.clients.delete(objectGuid);
|
||||||
|
const name = client.userName;
|
||||||
|
console.log(`${name} [${objectGuid}] manuell entfernt`);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a personal message to ObjectGUID
|
||||||
|
* @param {string} namespace - Namespace-Name
|
||||||
|
* @param {string} objectGuid - Socket-ID
|
||||||
|
* @param {string} event - Event-Name
|
||||||
|
* @param {any} data - Event-Data
|
||||||
|
* @param {object} [senderSocket] - optionaler Absender-Socket
|
||||||
|
*/
|
||||||
|
sendTo(namespace, objectGuid, event, data, senderSocket = null) {
|
||||||
|
const clients = this.clients.get(namespace);
|
||||||
|
if (!clients) return;
|
||||||
|
|
||||||
|
// Antwort an Absender
|
||||||
|
if (!objectGuid && senderSocket) {
|
||||||
|
senderSocket.emit(event, data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = clients.get(objectGuid);
|
||||||
|
if (!client?.socket) return;
|
||||||
|
|
||||||
|
client.socket.emit(event, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SocketManager;
|
||||||
|
|
||||||
|
// const WebSocket = require("ws");
|
||||||
|
// const url = require("url");
|
||||||
|
|
||||||
|
// class SocketManager {
|
||||||
|
// constructor() {
|
||||||
|
// this.namespaces = new Map(); // "/admin" → { clients:Set, handlers:Map }
|
||||||
|
// this.clients = new Map(); // objectGuid → { userName, socket }
|
||||||
|
|
||||||
|
// // Default-Namespaces wie bei Socket.IO
|
||||||
|
// this.add("/");
|
||||||
|
// this.add("/admin");
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Namespace erstellen
|
||||||
|
// */
|
||||||
|
// add(namespace) {
|
||||||
|
// if (this.namespaces.has(namespace)) return this.namespaces.get(namespace);
|
||||||
|
|
||||||
|
// const ns = {
|
||||||
|
// clients: new Set(),
|
||||||
|
// handlers: new Map() // event → callback
|
||||||
|
// };
|
||||||
|
|
||||||
|
// this.namespaces.set(namespace, ns);
|
||||||
|
// return ns;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Upgrade (HTTP → WebSocket) pro Namespace auswerten
|
||||||
|
// */
|
||||||
|
// handleUpgrade(namespace, req, socket, head) {
|
||||||
|
// if (!this.namespaces.has(namespace)) {
|
||||||
|
// socket.destroy();
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const ns = this.namespaces.get(namespace);
|
||||||
|
|
||||||
|
// const wss = new WebSocket.Server({ noServer: true });
|
||||||
|
|
||||||
|
// wss.handleUpgrade(req, socket, head, ws => {
|
||||||
|
// this._onConnection(namespace, ws, req);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Verbindung herstellen + authentifizieren
|
||||||
|
// */
|
||||||
|
// _onConnection(namespace, socket, req) {
|
||||||
|
// const params = new URLSearchParams(url.parse(req.url).query);
|
||||||
|
// const objectGuid = params.get("objectGuid");
|
||||||
|
// const sAMAccountName = params.get("sAMAccountName") || "Unknown";
|
||||||
|
|
||||||
|
// socket.namespace = namespace;
|
||||||
|
// socket.objectGuid = objectGuid;
|
||||||
|
// socket.userName = sAMAccountName;
|
||||||
|
|
||||||
|
// // Helper zum JSON-Senden
|
||||||
|
// socket.sendJSON = (obj) => socket.send(JSON.stringify(obj));
|
||||||
|
|
||||||
|
// const ns = this.namespaces.get(namespace);
|
||||||
|
// ns.clients.add(socket);
|
||||||
|
|
||||||
|
// if (objectGuid) {
|
||||||
|
// this.clients.set(objectGuid, { userName: sAMAccountName, socket: socket });
|
||||||
|
// console.log(`${sAMAccountName} [${objectGuid}] connected to [${namespace}]`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Nachrichten-Handling
|
||||||
|
// socket.on("message", raw => {
|
||||||
|
// let msg;
|
||||||
|
// try { msg = JSON.parse(raw); }
|
||||||
|
// catch { return; }
|
||||||
|
|
||||||
|
// const event = msg.event;
|
||||||
|
// const data = msg.data;
|
||||||
|
|
||||||
|
// const cb = ns.handlers.get(event);
|
||||||
|
// if (cb) cb(socket, data);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Disconnect
|
||||||
|
// socket.on("close", () => {
|
||||||
|
// ns.clients.delete(socket);
|
||||||
|
|
||||||
|
// if (objectGuid) {
|
||||||
|
// this.clients.delete(objectGuid);
|
||||||
|
// console.log(`${sAMAccountName} [${objectGuid}] disconnected from namespace ${namespace}`);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Events registrieren wie socket.on("event")
|
||||||
|
// */
|
||||||
|
// on(namespace, event, callback) {
|
||||||
|
// const ns = this.namespaces.get(namespace);
|
||||||
|
// if (!ns) return console.warn(`Namespace ${namespace} doesn't exist`);
|
||||||
|
// ns.handlers.set(event, callback);
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Broadcast in Namespace
|
||||||
|
// */
|
||||||
|
// broadcast(namespace, event, data) {
|
||||||
|
// const ns = this.namespaces.get(namespace);
|
||||||
|
// if (!ns) return;
|
||||||
|
|
||||||
|
// for (const client of ns.clients) {
|
||||||
|
// if (client.readyState === WebSocket.OPEN) {
|
||||||
|
// client.sendJSON({ event, data });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Persönliche Nachricht an GUID
|
||||||
|
// */
|
||||||
|
// sendTo(namespace, objectGuid, event, data, sender = null) {
|
||||||
|
// if (!objectGuid && sender) {
|
||||||
|
// sender.sendJSON({ event, data });
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const client = this.clients.get(objectGuid);
|
||||||
|
// if (!client) return;
|
||||||
|
|
||||||
|
// if (client.socket.readyState === WebSocket.OPEN) {
|
||||||
|
// client.socket.sendJSON({ event, data });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Client manuell entfernen
|
||||||
|
// */
|
||||||
|
// unregisterClient(objectGuid) {
|
||||||
|
// const client = this.clients.get(objectGuid);
|
||||||
|
// if (!client) return;
|
||||||
|
|
||||||
|
// client.socket.close(4000, "manual kick");
|
||||||
|
// this.clients.delete(objectGuid);
|
||||||
|
|
||||||
|
// console.log(`${client.userName} [${objectGuid}] manually removed`);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// module.exports = SocketManager;
|
||||||
139
src/services/sqlManager.js
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
const Sequelize = require('sequelize');
|
||||||
|
|
||||||
|
class SqlManager {
|
||||||
|
constructor() {
|
||||||
|
if (SqlManager._instance) return SqlManager._instance;
|
||||||
|
this.instances = {};
|
||||||
|
SqlManager._instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Neue MSSQL Instanz registrieren
|
||||||
|
*/
|
||||||
|
addInstance(name, config) {
|
||||||
|
if (this.instances[name]) {
|
||||||
|
console.log(`[INFO] Instance "${name}" already exists.`);
|
||||||
|
return this.instances[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sequelize = new Sequelize(config.database, config.user, config.password, {
|
||||||
|
host: config.host,
|
||||||
|
dialect: 'mssql',
|
||||||
|
port: config.port || 1433,
|
||||||
|
logging: config.logging || false,
|
||||||
|
pool: {
|
||||||
|
max: 10,
|
||||||
|
min: 0,
|
||||||
|
acquire: 30000,
|
||||||
|
idle: 10000
|
||||||
|
},
|
||||||
|
dialectOptions: {
|
||||||
|
options: {
|
||||||
|
encrypt: false,
|
||||||
|
trustServerCertificate: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.instances[name] = { sequelize, config };
|
||||||
|
return sequelize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instanz abrufen
|
||||||
|
*/
|
||||||
|
getInstance(name) {
|
||||||
|
const inst = this.instances[name];
|
||||||
|
if (!inst) {
|
||||||
|
throw new Error(`Instance "${name}" not found.`);
|
||||||
|
}
|
||||||
|
return inst.sequelize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconnect-Logik (falls Verbindung verloren geht)
|
||||||
|
*/
|
||||||
|
async reconnect(name) {
|
||||||
|
console.log(`[WARN] Reconnecting to ${name}...`);
|
||||||
|
|
||||||
|
const old = this.instances[name];
|
||||||
|
if (!old) throw new Error(`Instance "${name}" not found`);
|
||||||
|
|
||||||
|
const { config } = old;
|
||||||
|
|
||||||
|
// alte Verbindung killen
|
||||||
|
try { await old.sequelize.close(); } catch {}
|
||||||
|
|
||||||
|
// neu verbinden
|
||||||
|
const newSequelize = new Sequelize(
|
||||||
|
config.database,
|
||||||
|
config.username,
|
||||||
|
config.password,
|
||||||
|
{
|
||||||
|
...old.sequelize.options
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.instances[name].sequelize = newSequelize;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await newSequelize.authenticate();
|
||||||
|
console.log(`[OK] Reconnected to ${name}`);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[ERROR] Reconnect to ${name} failed:`, err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query ausführen (async)
|
||||||
|
*/
|
||||||
|
async query(instanceName, sql, options) {
|
||||||
|
const sequelize = this.getInstance(instanceName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [result] = await sequelize.query(sql, options);
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[ERROR] Query failed on "${instanceName}":`, err.message);
|
||||||
|
|
||||||
|
if (err.original && err.original.code === "ECONNCLOSED") {
|
||||||
|
await this.reconnect(instanceName);
|
||||||
|
return this.query(instanceName, sql, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pseudo-synchrones Query (Wirkung wie .querySync, intern aber async)
|
||||||
|
* Du wartest darauf, also fühlt es sich synchron an.
|
||||||
|
*/
|
||||||
|
querySync(instanceName, sql, options) {
|
||||||
|
return this.query(instanceName, sql, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verbindung testen
|
||||||
|
*/
|
||||||
|
async test(instanceName) {
|
||||||
|
const sequelize = this.getInstance(instanceName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
return {
|
||||||
|
levelId: 0,
|
||||||
|
message: `${sequelize.config.database} database connection hergestellt`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
levelId: 0,
|
||||||
|
message: [`Unable to connect to ${sequelize.config.database}`, error]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SqlManager;
|
||||||
14
src/sockets/adminSocket.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module.exports = (app, socketManager, namespace, eventManager) => {
|
||||||
|
const adminSocket = socketManager.namespaces.get(namespace);
|
||||||
|
|
||||||
|
// socketManager.on(namespace, 'plugin_status', (socket, data) => {
|
||||||
|
// eventManger.write(data.objectGuid, data.levelId, data.pluginName, app.locals.configuration.debug, data.message)
|
||||||
|
// })
|
||||||
|
|
||||||
|
socketManager.on(namespace, 'eventlog', (socket, data) => {
|
||||||
|
eventManager.write(data.objectGuid, data.levelId, data.pluginName, data.message)
|
||||||
|
});
|
||||||
|
|
||||||
|
socketManager.on(namespace, 'heartbeat', () => {
|
||||||
|
})
|
||||||
|
}
|
||||||
25
src/sockets/mainSocket.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const startMenuItemContext = require('@models/integratedStartmenuItems.js')
|
||||||
|
|
||||||
|
module.exports = (app, socketManager, namespace, pluginManager, authenticationModel, fileSystemManager, eventManager, activeDirectory) => {
|
||||||
|
const mainSocket = socketManager.namespaces.get(namespace);
|
||||||
|
|
||||||
|
socketManager.on(namespace, 'heartbeat', () => setInterval(() => console.log('test'), 1000));
|
||||||
|
|
||||||
|
socketManager.on(namespace, 'event', (socket, data) => {
|
||||||
|
socket.emit('event', { pluginName: data.pluginName, datetime: global.dateFormat(new Date(), 'dd.mm.yyyy HH:MM:SS'), levelId: data.levelId, message: data.message });
|
||||||
|
// eventManager.write(data.objectGuid, data.levelId, data.pluginName, data.message)
|
||||||
|
});
|
||||||
|
|
||||||
|
// global.json.configuration.onChange(info => {
|
||||||
|
// console.log(info.delta)
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
mainSocket.on('connection', socket => {
|
||||||
|
socket.on('changePartial', ({ partial, data }) => {
|
||||||
|
socket.emit('updatePartial', { partial, data });
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
156
utils.js
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
const { exec } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
const { permission } = require('process');
|
||||||
|
const { dirname } = require('path');
|
||||||
|
const { File: HotReload } = require(`@services/hotReload.js`);
|
||||||
|
const { service } = require(`@root/server.js`);
|
||||||
|
let integratedStartmenuItems = require('@models/integratedStartmenuItems');
|
||||||
|
|
||||||
|
|
||||||
|
global.path = {
|
||||||
|
root: dirname(require.main.filename),
|
||||||
|
source:`${dirname(require.main.filename)}/src`,
|
||||||
|
public: `${dirname(require.main.filename)}/public`,
|
||||||
|
plugins: `${dirname(require.main.filename)}/plugins`
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
global.json = {
|
||||||
|
releaseNotes: new HotReload(path.join(global.path.source, 'models', 'releasenotes.json')),
|
||||||
|
configuration: new HotReload(path.join(global.path.source, 'models', 'configuration.json')),
|
||||||
|
stylesheet: new HotReload(path.join(global.path.source, 'models', 'stylesheet.json')),
|
||||||
|
indexRoutes: new HotReload(path.join(global.path.source, 'routes', 'indexRoutes.js'), { historyLimit: 50, fileType: 'js' }),
|
||||||
|
startMenuItems: new HotReload(path.join(global.path.source, 'models', 'integratedStartmenuItems.js'), { historyLimit: 50, fileType: 'js' })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = startMenuItems = async function(app, sAMAccountName) {
|
||||||
|
function safeClone(obj) {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
delete integratedStartmenuItems;
|
||||||
|
|
||||||
|
integratedStartmenuItems = safeClone(json.startMenuItems.live);
|
||||||
|
|
||||||
|
const plugins = service
|
||||||
|
.get('pluginManager')
|
||||||
|
.getStatus()
|
||||||
|
.map(({ config, ...plugin }) => ({
|
||||||
|
...safeClone(plugin),
|
||||||
|
section: 'Plugin'
|
||||||
|
}));
|
||||||
|
|
||||||
|
let getAllPlugins = [...plugins, ...integratedStartmenuItems];
|
||||||
|
|
||||||
|
for (const plugin of getAllPlugins) {
|
||||||
|
|
||||||
|
plugin.menu.items = await Promise.all(
|
||||||
|
(plugin.menu.items || []).map(async item => {
|
||||||
|
|
||||||
|
const authorized =
|
||||||
|
item.label === 'hr' ||
|
||||||
|
item.permissions.includes('Administration')
|
||||||
|
? global.json.configuration.live.administration.includes(sAMAccountName)
|
||||||
|
: item.permissions.includes('*') ||
|
||||||
|
(
|
||||||
|
await Promise.all(
|
||||||
|
item.permissions.map(async permission =>
|
||||||
|
(await service.get('activeDirectoryManager').getGroup(permission)) &&
|
||||||
|
(await service.get('activeDirectoryManager').isUserMemberOfRecursive(sAMAccountName, permission))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).some(Boolean);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...safeClone(item),
|
||||||
|
authorized
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
plugin.onlyAdministration =
|
||||||
|
plugin.menu.items.every(item => !item.authorized) &&
|
||||||
|
!global.json.configuration.live.administration.includes(sAMAccountName);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllPlugins = getAllPlugins
|
||||||
|
.filter(plugin => !plugin.onlyAdministration)
|
||||||
|
.filter(plugin => plugin.active);
|
||||||
|
|
||||||
|
app.locals.startMenuItems = getAllPlugins;
|
||||||
|
|
||||||
|
return [...getAllPlugins];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert date into custom dateformat
|
||||||
|
* @param {any} date - Valid date as datetype or string
|
||||||
|
* @param {string} [format] - (optional) date characters are lowercase, time characters are uppercase; If is null, format will be "dd.mm.yyyy HH:MM:SS"
|
||||||
|
*/
|
||||||
|
module.exports = dateFormat = function(date, format = null) {
|
||||||
|
format = (format == null ? "dd.mm.yyyy HH:MM:SS" : format);
|
||||||
|
const finish_date = new Date(date);
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace('yyyy', finish_date.getFullYear())
|
||||||
|
.replace('yy', finish_date.getFullYear().toString().slice(-2))
|
||||||
|
.replace('mm', ("0" + (finish_date.getMonth() + 1)).slice(-2))
|
||||||
|
.replace('dd', ("0" + finish_date.getDate()).slice(-2))
|
||||||
|
.replace('HH', ("0" + finish_date.getHours()).slice(-2))
|
||||||
|
.replace('MM', ("0" + finish_date.getMinutes()).slice(-2))
|
||||||
|
.replace('SS', ("0" + finish_date.getSeconds()).slice(-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limits the number of functions that can be executed in parallel.
|
||||||
|
*/
|
||||||
|
// module.exports = createLimiter = function(max) {
|
||||||
|
// let active = 0;
|
||||||
|
// const queue = [];
|
||||||
|
|
||||||
|
// const runNext = () => {
|
||||||
|
// if (active >= max || queue.length === 0) return;
|
||||||
|
|
||||||
|
// active++;
|
||||||
|
// const { fn, resolve, reject } = queue.shift();
|
||||||
|
|
||||||
|
// fn()
|
||||||
|
// .then(resolve)
|
||||||
|
// .catch(reject)
|
||||||
|
// .finally(() => {
|
||||||
|
// active--;
|
||||||
|
// runNext();
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return fn =>
|
||||||
|
// new Promise((resolve, reject) => {
|
||||||
|
// queue.push({ fn, resolve, reject });
|
||||||
|
// runNext();
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* try to call an existing host by name or ip adress
|
||||||
|
* @param {string} hostname - Valid hostname or ip adress
|
||||||
|
* @param {string} [format] - (optional) date characters are lowercase, time characters are uppercase; If is null, format will be "dd.mm.yyyy HH:MM:SS"
|
||||||
|
*/
|
||||||
|
module.exports = ping = function(hostname, cbErr, cb, ttl = 1) {
|
||||||
|
exec(`ping ${hostname} -c 1 -W ${ttl}`, function (err, stdout, stderr) {
|
||||||
|
if(err) {
|
||||||
|
cbErr(err);
|
||||||
|
} else {
|
||||||
|
cb(stdout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = isObject = function(param) {
|
||||||
|
return param !== null && typeof param === 'object' && !Array.isArray(param);
|
||||||
|
}
|
||||||