diff --git a/backend/app.py b/backend/app.py index d37494d..1f605db 100644 --- a/backend/app.py +++ b/backend/app.py @@ -23,6 +23,14 @@ from tools import ( color_blueprint, json_formatter_blueprint, regex_blueprint, + hashverifier_blueprint, + url_blueprint, + stringutils_blueprint, + cron_blueprint, + ipcalc_blueprint, + lorem_blueprint, + csv_blueprint, + notes_blueprint, ) from admin import admin_bp @@ -44,6 +52,14 @@ app.register_blueprint(markdown_blueprint) app.register_blueprint(color_blueprint) app.register_blueprint(json_formatter_blueprint) app.register_blueprint(regex_blueprint) +app.register_blueprint(hashverifier_blueprint) +app.register_blueprint(url_blueprint) +app.register_blueprint(stringutils_blueprint) +app.register_blueprint(cron_blueprint) +app.register_blueprint(ipcalc_blueprint) +app.register_blueprint(lorem_blueprint) +app.register_blueprint(csv_blueprint) +app.register_blueprint(notes_blueprint) app.register_blueprint(admin_bp) # 🌐 React-Frontend ausliefern diff --git a/backend/tools/__init__.py b/backend/tools/__init__.py index 4a71b14..ce667b6 100644 --- a/backend/tools/__init__.py +++ b/backend/tools/__init__.py @@ -10,3 +10,11 @@ from .markdown_tool import markdown_blueprint from .colorconverter import color_blueprint from .jsonformatter import json_formatter_blueprint from .regextester import regex_blueprint +from .hashverifier import hashverifier_blueprint +from .urltool import url_blueprint +from .stringutils import stringutils_blueprint +from .cronexplainer import cron_blueprint +from .ipcalc import ipcalc_blueprint +from .loremipsum import lorem_blueprint +from .csvviewer import csv_blueprint +from .notes import notes_blueprint diff --git a/backend/tools/cronexplainer.py b/backend/tools/cronexplainer.py new file mode 100644 index 0000000..33f3f83 --- /dev/null +++ b/backend/tools/cronexplainer.py @@ -0,0 +1,166 @@ +from flask import Blueprint, request, jsonify +from datetime import datetime, timedelta +from util.logger import logger +from auth.token import verify_token + +cron_blueprint = Blueprint('cron_tool', __name__) + +MONTH_NAMES = { + '1': 'Januar', '2': 'Februar', '3': 'März', '4': 'April', + '5': 'Mai', '6': 'Juni', '7': 'Juli', '8': 'August', + '9': 'September', '10': 'Oktober', '11': 'November', '12': 'Dezember', +} +WEEKDAY_NAMES = { + '0': 'Sonntag', '1': 'Montag', '2': 'Dienstag', '3': 'Mittwoch', + '4': 'Donnerstag', '5': 'Freitag', '6': 'Samstag', '7': 'Sonntag', +} + + +def parse_field(field, min_val, max_val): + values = set() + for part in field.split(','): + part = part.strip() + if part == '*': + values.update(range(min_val, max_val + 1)) + elif '/' in part: + base, step = part.split('/', 1) + step = int(step) + if base == '*': + start, end = min_val, max_val + elif '-' in base: + s, e = base.split('-') + start, end = int(s), int(e) + else: + start, end = int(base), max_val + values.update(range(start, end + 1, step)) + elif '-' in part: + s, e = part.split('-') + values.update(range(int(s), int(e) + 1)) + else: + values.add(int(part)) + return sorted(v for v in values if min_val <= v <= max_val) + + +def explain_field_text(field, unit, plural, labels=None): + if field == '*': + return f"Jede(n) {unit}" + parts = field.split(',') + described = [] + for p in parts: + if p.startswith('*/'): + described.append(f"alle {p[2:]} {plural}") + elif '-' in p and '/' not in p: + a, b = p.split('-', 1) + a_l = labels.get(a, a) if labels else a + b_l = labels.get(b, b) if labels else b + described.append(f"{a_l} bis {b_l}") + elif '/' in p: + base, step = p.split('/', 1) + if '-' in base: + a, b = base.split('-', 1) + described.append(f"{a}–{b} alle {step}") + else: + described.append(f"ab {base} alle {step}") + else: + described.append(labels.get(p, p) if labels else p) + return ', '.join(described) + + +def build_summary(minute, hour, day, month, weekday): + parts = [] + + if minute == '*' and hour == '*': + parts.append("Jede Minute") + elif minute.startswith('*/') and hour == '*': + parts.append(f"Alle {minute[2:]} Minuten") + elif hour != '*' and minute == '0': + h = explain_field_text(hour, 'Stunde', 'Stunden') + parts.append(f"Um {h}:00 Uhr") + else: + parts.append(f"Minute: {minute}, Stunde: {hour}") + + if weekday != '*' and day == '*': + wd = explain_field_text(weekday, 'Wochentag', 'Wochentage', WEEKDAY_NAMES) + parts.append(f"jeden {wd}") + elif day != '*' and weekday == '*': + parts.append(f"am {day}. des Monats") + elif day != '*' and weekday != '*': + wd = explain_field_text(weekday, 'Wochentag', 'Wochentage', WEEKDAY_NAMES) + parts.append(f"am {day}. oder {wd}") + + if month != '*': + m = explain_field_text(month, 'Monat', 'Monate', MONTH_NAMES) + parts.append(f"im {m}") + + return ', '.join(parts) + + +def get_next_runs(minute_vals, hour_vals, day_vals, month_vals, weekday_vals): + # cron weekday 0=Sun..6=Sat,7=Sun -> python weekday 0=Mon..6=Sun + py_weekdays = set((wd + 6) % 7 for wd in weekday_vals) + minute_set = set(minute_vals) + hour_set = set(hour_vals) + day_set = set(day_vals) + month_set = set(month_vals) + + now = datetime.now().replace(second=0, microsecond=0) + timedelta(minutes=1) + results = [] + current = now + + for _ in range(2 * 366 * 24 * 60): + if len(results) >= 5: + break + if (current.month in month_set and + current.day in day_set and + current.weekday() in py_weekdays and + current.hour in hour_set and + current.minute in minute_set): + results.append(current.isoformat()) + current += timedelta(minutes=1) + + return results + + +@cron_blueprint.route('/api/cron/explain', methods=['POST']) +def explain_cron(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() or {} + expression = data.get("expression", "").strip() + + fields = expression.split() + if len(fields) != 5: + return jsonify({"error": "Cron-Ausdruck muss genau 5 Felder haben (Minute Stunde Tag Monat Wochentag)"}), 400 + + minute_f, hour_f, day_f, month_f, weekday_f = fields + + try: + minute_vals = parse_field(minute_f, 0, 59) + hour_vals = parse_field(hour_f, 0, 23) + day_vals = parse_field(day_f, 1, 31) + month_vals = parse_field(month_f, 1, 12) + weekday_vals = parse_field(weekday_f, 0, 7) + except (ValueError, ZeroDivisionError) as e: + return jsonify({"error": f"Ungültiger Cron-Ausdruck: {e}"}), 400 + + field_descriptions = { + "minute": explain_field_text(minute_f, 'Minute', 'Minuten'), + "hour": explain_field_text(hour_f, 'Stunde', 'Stunden'), + "day": explain_field_text(day_f, 'Tag', 'Tage'), + "month": explain_field_text(month_f, 'Monat', 'Monate', MONTH_NAMES), + "weekday": explain_field_text(weekday_f, 'Wochentag', 'Wochentage', WEEKDAY_NAMES), + } + + next_runs = get_next_runs(minute_vals, hour_vals, day_vals, month_vals, weekday_vals) + + return jsonify({ + "explanation": build_summary(minute_f, hour_f, day_f, month_f, weekday_f), + "fields": field_descriptions, + "next_runs": next_runs, + }) + + except Exception as e: + logger.error(f"Fehler cron explain: {e}") + return jsonify({"error": "Fehler beim Verarbeiten des Ausdrucks"}), 500 diff --git a/backend/tools/csvviewer.py b/backend/tools/csvviewer.py new file mode 100644 index 0000000..d6180fc --- /dev/null +++ b/backend/tools/csvviewer.py @@ -0,0 +1,50 @@ +from flask import Blueprint, request, jsonify +import csv +import io +from util.logger import logger +from auth.token import verify_token + +csv_blueprint = Blueprint('csv_tool', __name__) + +MAX_ROWS = 500 + + +@csv_blueprint.route('/api/csv/parse', methods=['POST']) +def parse_csv(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() or {} + text = data.get("text", "") + delimiter = data.get("delimiter", ",") + + # Handle escaped tab + if delimiter == "\\t" or delimiter == "\t": + delimiter = "\t" + if not delimiter: + delimiter = "," + + reader = csv.reader(io.StringIO(text), delimiter=delimiter) + all_rows = list(reader) + + if not all_rows: + return jsonify({"headers": [], "rows": [], "total_rows": 0, "truncated": False}) + + headers = all_rows[0] + data_rows = all_rows[1:] + total_rows = len(data_rows) + truncated = total_rows > MAX_ROWS + + return jsonify({ + "headers": headers, + "rows": data_rows[:MAX_ROWS], + "total_rows": total_rows, + "truncated": truncated, + }) + + except csv.Error as e: + return jsonify({"message": f"Ungültiges CSV: {e}"}), 400 + except Exception as e: + logger.error(f"Fehler csvviewer: {e}") + return jsonify({"message": "Fehler beim Verarbeiten des CSV"}), 500 diff --git a/backend/tools/hashverifier.py b/backend/tools/hashverifier.py new file mode 100644 index 0000000..ed4bbc3 --- /dev/null +++ b/backend/tools/hashverifier.py @@ -0,0 +1,39 @@ +from flask import Blueprint, request, jsonify +import hashlib +import bcrypt +from util.logger import logger +from auth.token import verify_token + +hashverifier_blueprint = Blueprint('hashverifier_tool', __name__) + + +@hashverifier_blueprint.route('/api/hash/verify', methods=['POST']) +def verify_hash(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() or {} + text = data.get("text", "") + hash_val = data.get("hash", "").strip() + algorithm = data.get("algorithm", "sha256") + + if algorithm == "md5": + computed = hashlib.md5(text.encode()).hexdigest() + match = computed.lower() == hash_val.lower() + elif algorithm == "sha256": + computed = hashlib.sha256(text.encode()).hexdigest() + match = computed.lower() == hash_val.lower() + elif algorithm == "bcrypt": + try: + match = bcrypt.checkpw(text.encode(), hash_val.encode()) + except Exception: + return jsonify({"message": "Ungültiger bcrypt-Hash"}), 400 + else: + return jsonify({"message": "Unbekannter Algorithmus"}), 400 + + logger.info(f"Hash verify ({algorithm}) von {user['username']}: {match}") + return jsonify({"match": match}) + except Exception as e: + logger.error(f"Fehler bei Hash-Verifikation: {e}") + return jsonify({"message": "Fehler bei der Verifikation"}), 500 diff --git a/backend/tools/ipcalc.py b/backend/tools/ipcalc.py new file mode 100644 index 0000000..f493236 --- /dev/null +++ b/backend/tools/ipcalc.py @@ -0,0 +1,49 @@ +from flask import Blueprint, request, jsonify +import ipaddress +from util.logger import logger +from auth.token import verify_token + +ipcalc_blueprint = Blueprint('ipcalc_tool', __name__) + + +@ipcalc_blueprint.route('/api/ip/calculate', methods=['POST']) +def ip_calculate(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() or {} + cidr = data.get("cidr", "").strip() + + try: + network = ipaddress.IPv4Network(cidr, strict=False) + except ValueError as e: + return jsonify({"message": f"Ungültige CIDR-Notation: {e}"}), 400 + + hosts = list(network.hosts()) + first_host = str(hosts[0]) if hosts else str(network.network_address) + last_host = str(hosts[-1]) if hosts else str(network.broadcast_address) + total_hosts = len(hosts) + + # Wildcard = inverse of netmask + netmask_int = int(network.netmask) + wildcard_int = (~netmask_int) & 0xFFFFFFFF + wildcard = str(ipaddress.IPv4Address(wildcard_int)) + + ip_class = "Privat" if network.is_private else "Öffentlich" + + return jsonify({ + "network": str(network.network_address), + "broadcast": str(network.broadcast_address), + "netmask": str(network.netmask), + "wildcard": wildcard, + "first_host": first_host, + "last_host": last_host, + "total_hosts": total_hosts, + "prefix_length": network.prefixlen, + "ip_class": ip_class, + }) + + except Exception as e: + logger.error(f"Fehler ipcalc: {e}") + return jsonify({"message": "Fehler bei der Berechnung"}), 500 diff --git a/backend/tools/loremipsum.py b/backend/tools/loremipsum.py new file mode 100644 index 0000000..5250b42 --- /dev/null +++ b/backend/tools/loremipsum.py @@ -0,0 +1,63 @@ +from flask import Blueprint, request, jsonify +import random +from util.logger import logger +from auth.token import verify_token + +lorem_blueprint = Blueprint('lorem_tool', __name__) + +WORDS = [ + "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", + "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore", + "magna", "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud", + "exercitation", "ullamco", "laboris", "nisi", "aliquip", "ex", "ea", "commodo", + "consequat", "duis", "aute", "irure", "reprehenderit", "voluptate", "velit", + "esse", "cillum", "eu", "fugiat", "nulla", "pariatur", "excepteur", "sint", + "occaecat", "cupidatat", "non", "proident", "sunt", "culpa", "qui", "officia", + "deserunt", "mollit", "anim", "id", "est", "laborum", "perspiciatis", "unde", + "omnis", "iste", "natus", "error", "voluptatem", "accusantium", "doloremque", + "laudantium", "totam", "rem", "aperiam", "eaque", "ipsa", "quae", "ab", "illo", + "inventore", "veritatis", "quasi", "architecto", "beatae", "vitae", "dicta", + "explicabo", "nemo", "ipsam", "quia", "voluptas", "aspernatur", "odit", + "fugit", "magni", "dolores", "ratione", "sequi", "nesciunt", "neque", "porro", + "quisquam", "adipisci", "numquam", "eius", "modi", "tempora", "incidunt", + "soluta", "nobis", "eligendi", "optio", "cumque", "nihil", "impedit", "minus", + "maxime", "placeat", "facere", "possimus", "omnis", "assumenda", "repellendus", +] + + +def make_sentence(): + word_count = random.randint(8, 15) + words = [random.choice(WORDS) for _ in range(word_count)] + return words[0].capitalize() + ' ' + ' '.join(words[1:]) + '.' + + +def make_paragraph(): + sentence_count = random.randint(4, 6) + return ' '.join(make_sentence() for _ in range(sentence_count)) + + +@lorem_blueprint.route('/api/lorem/generate', methods=['POST']) +def generate_lorem(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() or {} + gen_type = data.get("type", "sentences") + count = int(data.get("count", 3)) + count = max(1, min(20, count)) + + if gen_type == "words": + text = ' '.join(random.choice(WORDS) for _ in range(count)) + elif gen_type == "sentences": + text = ' '.join(make_sentence() for _ in range(count)) + elif gen_type == "paragraphs": + text = '\n\n'.join(make_paragraph() for _ in range(count)) + else: + return jsonify({"message": "Ungültiger Typ"}), 400 + + return jsonify({"text": text}) + + except Exception as e: + logger.error(f"Fehler lorem ipsum: {e}") + return jsonify({"message": "Fehler bei der Generierung"}), 500 diff --git a/backend/tools/notes.py b/backend/tools/notes.py new file mode 100644 index 0000000..681db28 --- /dev/null +++ b/backend/tools/notes.py @@ -0,0 +1,139 @@ +from flask import Blueprint, request, jsonify +from datetime import datetime, timezone +from util.logger import logger +from util.db_pool import get_connection +from auth.token import verify_token + +notes_blueprint = Blueprint('notes_tool', __name__) + + +def ensure_table(): + conn = get_connection() + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS notes ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + title VARCHAR(255) NOT NULL, + content TEXT, + language VARCHAR(50) DEFAULT 'text', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + """) + conn.commit() + cursor.close() + conn.close() + + +@notes_blueprint.route('/api/notes', methods=['GET']) +def get_notes(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + ensure_table() + conn = get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT id, title, content, language, created_at, updated_at FROM notes WHERE user_id = %s ORDER BY updated_at DESC", + (user['id'],) + ) + notes = cursor.fetchall() + cursor.close() + conn.close() + for n in notes: + if n.get('created_at'): + n['created_at'] = n['created_at'].isoformat() + if n.get('updated_at'): + n['updated_at'] = n['updated_at'].isoformat() + return jsonify(notes) + except Exception as e: + logger.error(f"Fehler notes GET: {e}") + return jsonify({"message": "Fehler beim Laden"}), 500 + + +@notes_blueprint.route('/api/notes', methods=['POST']) +def create_note(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + ensure_table() + data = request.get_json() or {} + title = data.get("title", "Neue Notiz").strip() or "Neue Notiz" + content = data.get("content", "") + language = data.get("language", "text") + + conn = get_connection() + cursor = conn.cursor() + cursor.execute( + "INSERT INTO notes (user_id, title, content, language) VALUES (%s, %s, %s, %s)", + (user['id'], title, content, language) + ) + conn.commit() + note_id = cursor.lastrowid + cursor.close() + conn.close() + + logger.info(f"Notiz erstellt von {user['username']}: id={note_id}") + return jsonify({"id": note_id, "title": title, "content": content, "language": language}) + except Exception as e: + logger.error(f"Fehler notes POST: {e}") + return jsonify({"message": "Fehler beim Erstellen"}), 500 + + +@notes_blueprint.route('/api/notes/', methods=['PUT']) +def update_note(note_id): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() or {} + title = data.get("title", "").strip() or "Neue Notiz" + content = data.get("content", "") + language = data.get("language", "text") + now = datetime.now(timezone.utc).replace(tzinfo=None) + + conn = get_connection() + cursor = conn.cursor() + cursor.execute( + "UPDATE notes SET title=%s, content=%s, language=%s, updated_at=%s WHERE id=%s AND user_id=%s", + (title, content, language, now, note_id, user['id']) + ) + conn.commit() + affected = cursor.rowcount + cursor.close() + conn.close() + + if affected == 0: + return jsonify({"message": "Notiz nicht gefunden"}), 404 + return jsonify({"ok": True}) + except Exception as e: + logger.error(f"Fehler notes PUT: {e}") + return jsonify({"message": "Fehler beim Speichern"}), 500 + + +@notes_blueprint.route('/api/notes/', methods=['DELETE']) +def delete_note(note_id): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + conn = get_connection() + cursor = conn.cursor() + cursor.execute( + "DELETE FROM notes WHERE id=%s AND user_id=%s", + (note_id, user['id']) + ) + conn.commit() + affected = cursor.rowcount + cursor.close() + conn.close() + + if affected == 0: + return jsonify({"message": "Notiz nicht gefunden"}), 404 + return jsonify({"ok": True}) + except Exception as e: + logger.error(f"Fehler notes DELETE: {e}") + return jsonify({"message": "Fehler beim Löschen"}), 500 diff --git a/backend/tools/stringutils.py b/backend/tools/stringutils.py new file mode 100644 index 0000000..ba11f51 --- /dev/null +++ b/backend/tools/stringutils.py @@ -0,0 +1,53 @@ +from flask import Blueprint, request, jsonify +from collections import Counter +import re +from util.logger import logger +from auth.token import verify_token + +stringutils_blueprint = Blueprint('stringutils_tool', __name__) + + +@stringutils_blueprint.route('/api/string/analyze', methods=['POST']) +def string_analyze(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() or {} + text = data.get("text", "") + operation = data.get("operation", "stats") + + if operation == "stats": + words = text.split() if text.strip() else [] + lines = text.split('\n') + return jsonify({ + "operation": "stats", + "chars": len(text), + "chars_no_spaces": len(text.replace(' ', '')), + "words": len(words), + "lines": len(lines), + "spaces": text.count(' '), + }) + elif operation == "uppercase": + return jsonify({"operation": "uppercase", "result": text.upper()}) + elif operation == "lowercase": + return jsonify({"operation": "lowercase", "result": text.lower()}) + elif operation == "titlecase": + return jsonify({"operation": "titlecase", "result": text.title()}) + elif operation == "reverse": + return jsonify({"operation": "reverse", "result": text[::-1]}) + elif operation == "trim": + return jsonify({"operation": "trim", "result": text.strip()}) + elif operation == "remove_spaces": + return jsonify({"operation": "remove_spaces", "result": text.replace(' ', '')}) + elif operation == "count_words": + words = re.findall(r'\b\w+\b', text.lower()) + counter = Counter(words) + top10 = [{"word": w, "count": c} for w, c in counter.most_common(10)] + return jsonify({"operation": "count_words", "words": top10}) + else: + return jsonify({"message": "Unbekannte Operation"}), 400 + + except Exception as e: + logger.error(f"Fehler stringutils: {e}") + return jsonify({"message": "Fehler bei der Verarbeitung"}), 500 diff --git a/backend/tools/urltool.py b/backend/tools/urltool.py new file mode 100644 index 0000000..667c32e --- /dev/null +++ b/backend/tools/urltool.py @@ -0,0 +1,36 @@ +from flask import Blueprint, request, jsonify +from urllib.parse import quote, unquote +from util.logger import logger +from auth.token import verify_token + +url_blueprint = Blueprint('url_tool', __name__) + + +@url_blueprint.route('/api/url/encode', methods=['POST']) +def url_encode(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() or {} + text = data.get("text", "") + result = quote(text, safe='') + return jsonify({"result": result}) + except Exception as e: + logger.error(f"Fehler URL encode: {e}") + return jsonify({"message": "Fehler beim Encoding"}), 500 + + +@url_blueprint.route('/api/url/decode', methods=['POST']) +def url_decode(): + user = verify_token() + if not user: + return jsonify({"message": "Nicht autorisiert"}), 401 + try: + data = request.get_json() or {} + text = data.get("text", "") + result = unquote(text) + return jsonify({"result": result}) + except Exception as e: + logger.error(f"Fehler URL decode: {e}") + return jsonify({"message": "Fehler beim Decoding"}), 500 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 6c3d99a..6862a1e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -14,6 +14,14 @@ import MarkdownTool from './components/MarkdownTool'; import ColorConverterTool from './components/ColorConverterTool'; import JsonFormatterTool from './components/JsonFormatterTool'; import RegexTesterTool from './components/RegexTesterTool'; +import HashVerifierTool from './components/HashVerifierTool'; +import UrlTool from './components/UrlTool'; +import StringUtilsTool from './components/StringUtilsTool'; +import CronExplainerTool from './components/CronExplainerTool'; +import IpCalcTool from './components/IpCalcTool'; +import LoremIpsumTool from './components/LoremIpsumTool'; +import CsvViewerTool from './components/CsvViewerTool'; +import NotesTool from './components/NotesTool'; import NavBar from './components/NavBar'; import ToolOverview from './components/ToolOverview'; import AdminDashboard from './components/AdminDashboard'; @@ -60,7 +68,15 @@ function App() { : } /> : } /> : } /> - : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> { + const val = expr !== undefined ? expr : expression; + setError(''); + setResult(null); + if (!val.trim()) { + setError('Bitte Cron-Ausdruck eingeben.'); + return; + } + try { + const res = await axios.post('/api/cron/explain', { expression: val }); + setResult(res.data); + } catch (err) { + setError(err.response?.data?.error || err.response?.data?.message || 'Ungültiger Cron-Ausdruck'); + } + }; + + const useExample = (val) => { + setExpression(val); + setError(''); + setResult(null); + explain(val); + }; + + const formatDate = (iso) => { + const d = new Date(iso); + return d.toLocaleString('de-DE', { + weekday: 'short', day: '2-digit', month: '2-digit', + year: 'numeric', hour: '2-digit', minute: '2-digit', + }); + }; + + return ( +
+

Cron Erklärer

+

+ Cron-Ausdrücke (5 Felder) analysieren und auf Deutsch erklären. +

+ +
+ {EXAMPLES.map(({ label, value }) => ( + + ))} +
+ +
+ { setExpression(e.target.value); setResult(null); setError(''); }} + placeholder="z.B. 0 9 * * 1-5" + style={{ fontFamily: 'monospace', flex: 1 }} + onKeyDown={(e) => e.key === 'Enter' && explain()} + /> + +
+ + {error &&

{error}

} + + {result && ( + <> +
+

Erklärung

+

{result.explanation}

+
+ +
+

Felder

+ + + + + + + + + + {FIELD_LABELS.map(({ key, label }, i) => ( + + + + + + ))} + +
FeldAusdruckBedeutung
{label} + {expression.split(/\s+/)[i] || '—'} + + {result.fields[key]} +
+
+ + {result.next_runs && result.next_runs.length > 0 && ( +
+

Nächste 5 Ausführungen

+
+ {result.next_runs.map((run, i) => ( +
+ #{i + 1} + + {formatDate(run)} + +
+ ))} +
+
+ )} + + )} +
+ ); +} + +export default CronExplainerTool; diff --git a/frontend/src/components/CsvViewerTool.jsx b/frontend/src/components/CsvViewerTool.jsx new file mode 100644 index 0000000..e96e651 --- /dev/null +++ b/frontend/src/components/CsvViewerTool.jsx @@ -0,0 +1,158 @@ +import { useState } from 'react'; +import axios from '../services/api'; + +const DELIMITERS = [ + { label: 'Komma (,)', value: ',' }, + { label: 'Semikolon (;)', value: ';' }, + { label: 'Tab', value: '\\t' }, + { label: 'Pipe (|)', value: '|' }, +]; + +function CsvViewerTool() { + const [text, setText] = useState(''); + const [delimiter, setDelimiter] = useState(','); + const [result, setResult] = useState(null); + const [error, setError] = useState(''); + const [sortCol, setSortCol] = useState(null); + const [sortAsc, setSortAsc] = useState(true); + + const parse = async () => { + setError(''); + setResult(null); + setSortCol(null); + if (!text.trim()) { + setError('Bitte CSV-Inhalt eingeben.'); + return; + } + try { + const res = await axios.post('/api/csv/parse', { text, delimiter }); + setResult(res.data); + } catch (err) { + setError(err.response?.data?.message || 'Fehler beim Verarbeiten des CSV'); + } + }; + + const handleSort = (colIdx) => { + if (sortCol === colIdx) { + setSortAsc(!sortAsc); + } else { + setSortCol(colIdx); + setSortAsc(true); + } + }; + + const getSortedRows = () => { + if (!result) return []; + if (sortCol === null) return result.rows; + return [...result.rows].sort((a, b) => { + const av = a[sortCol] ?? ''; + const bv = b[sortCol] ?? ''; + const numA = parseFloat(av); + const numB = parseFloat(bv); + if (!isNaN(numA) && !isNaN(numB)) { + return sortAsc ? numA - numB : numB - numA; + } + return sortAsc ? av.localeCompare(bv) : bv.localeCompare(av); + }); + }; + + return ( +
+

CSV Viewer

+

+ CSV-Daten einfügen und als Tabelle anzeigen. +

+ +