diff --git a/app/__init__.py b/app/__init__.py index aa79e88..f9e1670 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -81,9 +81,11 @@ def create_app(): from .routes.auth_routes import auth_bp from .routes.admin_routes import admin_bp from .routes.printer_routes import printer_bp + from .utils.api_handle import api_bp app.register_blueprint(main_bp) app.register_blueprint(auth_bp) app.register_blueprint(admin_bp) app.register_blueprint(printer_bp) + app.register_blueprint(api_bp) return app diff --git a/app/assets/i18n/de.json b/app/assets/i18n/de.json index 6ddd61d..efff4a7 100644 --- a/app/assets/i18n/de.json +++ b/app/assets/i18n/de.json @@ -237,5 +237,28 @@ "Bridge infill": "Brückefüllung", "Top solid infill": "Oberste solide Füllung", "Others": "Andere", - "Are you sure you want to clear the board?": "Sind Sie sicher, dass Sie das Brett leeren möchten?" + "Are you sure you want to clear the board?": "Sind Sie sicher, dass Sie das Brett leeren möchten?", + "d": "t", + "h": "std", + "m": "m", + "s": "s", + "Auto Leveling": "Auto-Nivellierung", + "Account Management": "Kontoverwaltung", + "Change Password": "Passwort ändern", + "Current Password": "Aktuelles Passwort", + "Confirm New Password": "Neues Passwort bestätigen", + "Update Password": "Passwort aktualisieren", + "Active Sessions": "Aktive Sitzungen", + "Device": "Gerät", + "IP Address": "IP-Adresse", + "Last Active": "Zuletzt aktiv", + "Action": "Aktion", + "This Device": "Dieses Gerät", + "Unknown Device": "Unbekanntes Gerät", + "Are you sure you want to terminate this session?": "Sind Sie sicher, dass Sie diese Sitzung beenden möchten?", + "Logout from this device?": "Von diesem Gerät abmelden?", + "No active sessions found.": "Keine aktiven Sitzungen gefunden.", + "Please login to view the webcam stream.": "Bitte melden Sie sich an, um die Live-Kamera zu sehen.", + "Remember Me": "Erinnere dich an mich", + "Merge Guest Data": "Gästendaten zusammenführen" } \ No newline at end of file diff --git a/app/assets/i18n/en.json b/app/assets/i18n/en.json index 54eba44..cb1b6c6 100644 --- a/app/assets/i18n/en.json +++ b/app/assets/i18n/en.json @@ -237,5 +237,28 @@ "Bridge infill": "Bridge infill", "Top solid infill": "Top solid infill", "Others": "Others", - "Are you sure you want to clear the board?": "Are you sure you want to clear the board?" + "Are you sure you want to clear the board?": "Are you sure you want to clear the board?", + "d": "d", + "h": "h", + "m": "m", + "s": "s", + "Auto Leveling": "Auto Leveling", + "Account Management": "Account Management", + "Change Password": "Change Password", + "Current Password": "Current Password", + "Confirm New Password": "Confirm New Password", + "Update Password": "Update Password", + "Active Sessions": "Active Sessions", + "Device": "Device", + "IP Address": "IP Address", + "Last Active": "Last Active", + "Action": "Action", + "This Device": "This Device", + "Unknown Device": "Unknown Device", + "Are you sure you want to terminate this session?": "Are you sure you want to terminate this session?", + "Logout from this device?": "Logout from this device?", + "No active sessions found.": "No active sessions found.", + "Please login to view the webcam stream.": "Please login to view the webcam stream.", + "Remember Me": "Remember Me", + "Merge Guest Data": "Merge Guest Data" } \ No newline at end of file diff --git a/app/assets/i18n/zh-cn.json b/app/assets/i18n/zh-cn.json index 2fa8fdc..939cd55 100644 --- a/app/assets/i18n/zh-cn.json +++ b/app/assets/i18n/zh-cn.json @@ -237,5 +237,28 @@ "Bridge infill": "桥接填充", "Top solid infill": "顶部实体填充", "Others": "其他", - "Are you sure you want to clear the board?": "您确定要清空构建板吗?" + "Are you sure you want to clear the board?": "您确定要清空构建板吗?", + "d": "天", + "h": "时", + "m": "分", + "s": "秒", + "Auto Leveling": "自动调平", + "Account Management": "账号管理", + "Change Password": "修改密码", + "Current Password": "当前密码", + "Confirm New Password": "确认新密码", + "Update Password": "更新密码", + "Active Sessions": "活跃会话", + "Device": "设备", + "IP Address": "IP 地址", + "Last Active": "最后活跃", + "Action": "操作", + "This Device": "当前设备", + "Unknown Device": "未知设备", + "Are you sure you want to terminate this session?": "您确定要终止此会话吗?", + "Logout from this device?": "从此设备注销?", + "No active sessions found.": "未找到活跃的会话。", + "Please login to view the webcam stream.": "请登录以查看实时摄像头。", + "Remember Me": "记住我", + "Merge Guest Data": "合并访客数据" } \ No newline at end of file diff --git a/app/models.py b/app/models.py index 45529db..fc4e2f1 100644 --- a/app/models.py +++ b/app/models.py @@ -39,3 +39,9 @@ class SystemConfig(db.Model): id = db.Column(db.Integer, primary_key=True) key = db.Column(db.String(50), unique=True, nullable=False) value = db.Column(db.String(255), nullable=False) + +class ApiKey(db.Model): + id = db.Column(db.Integer, primary_key=True) + key = db.Column(db.String(100), unique=True, nullable=False) + name = db.Column(db.String(100), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) diff --git a/app/routes/admin_routes.py b/app/routes/admin_routes.py index a8d78c7..857ece8 100644 --- a/app/routes/admin_routes.py +++ b/app/routes/admin_routes.py @@ -191,3 +191,34 @@ def delete_user(user_id): + +@admin_bp.route('/api_keys') +def api_keys(): + from app.models import ApiKey + keys = ApiKey.query.order_by(ApiKey.created_at.desc()).all() + return render_template('admin/api_keys.html', keys=keys) + +@admin_bp.route('/api_key/add', methods=['POST']) +def add_api_key(): + from app.models import ApiKey + import secrets + name = request.form.get('name') + if not name: + flash("Name is required", "danger") + return redirect(url_for('admin.api_keys')) + + key_value = secrets.token_urlsafe(32) + new_key = ApiKey(name=name, key=key_value) + db.session.add(new_key) + db.session.commit() + flash(f"API Key '{name}' created.", "success") + return redirect(url_for('admin.api_keys')) + +@admin_bp.route('/api_key//delete', methods=['POST']) +def delete_api_key(key_id): + from app.models import ApiKey + key = ApiKey.query.get_or_404(key_id) + db.session.delete(key) + db.session.commit() + flash(f'API Key {key.name} deleted.', 'success') + return redirect(url_for('admin.api_keys')) diff --git a/app/routes/main_routes.py b/app/routes/main_routes.py index 0013c11..a3a2675 100644 --- a/app/routes/main_routes.py +++ b/app/routes/main_routes.py @@ -298,7 +298,18 @@ def preview_gcode(file_id): content = "File not found or not ready." line_count = 0 + + time_info = "-" + layer1_time = "-" + filament_used = "-" + if os.path.exists(filepath): + from app.utils.gcode_parser import get_gcode_metadata + metadata = get_gcode_metadata(filepath) + time_info = metadata.get('print_time', '-') + layer1_time = metadata.get('first_layer_time', '-') + filament_used = metadata.get('filament_used', '-') + with open(filepath, 'r') as f: lines = f.readlines() line_count = len(lines) @@ -313,7 +324,8 @@ def preview_gcode(file_id): configs = {c.key: c.value for c in SystemConfig.query.all()} offset_x = float(configs.get('offset_x', '0.0')) offset_y = float(configs.get('offset_y', '0.0')) - return render_template('slice/gcode_preview.html', file=print_file, content=content, line_count=line_count, machine_width=w, machine_depth=h, machine_height=hd, offset_x=offset_x, offset_y=offset_y) + return render_template('slice/gcode_preview.html', file=print_file, content=content, line_count=line_count, machine_width=w, machine_depth=h, machine_height=hd, offset_x=offset_x, offset_y=offset_y, + time_info=time_info, layer1_time=layer1_time, filament_used=filament_used) @main_bp.route('/delete_file/', methods=['POST']) @login_required diff --git a/app/routes/printer_routes.py b/app/routes/printer_routes.py index 93db43b..bb11199 100644 --- a/app/routes/printer_routes.py +++ b/app/routes/printer_routes.py @@ -23,9 +23,15 @@ def _enrich_job_data(job_data): if job_data and job_data.get('job', {}).get('file', {}).get('name'): from app.models import PrintFile internal_name = job_data['job']['file']['name'] - print_file = PrintFile.query.filter_by(filename=internal_name).first() - if print_file and print_file.original_filename: - job_data['job']['file']['display_name'] = print_file.original_filename + internal_stl_name = str(internal_name)[:-5]+"stl" + user_files = {} + print_file = None + for f in PrintFile.query.filter_by(user_id=current_user.id, status='sliced').all(): + user_files[f.filename] = f.original_filename + if internal_stl_name in user_files.keys(): + print_file = user_files[str(internal_name)[:-5]+"stl"] + if print_file: + job_data['job']['file']['display_name'] = print_file else: job_data['job']['file']['display_name'] = internal_name return job_data @@ -40,9 +46,13 @@ def status(): if client: try: status_data = client.get_printer_status() + print(status_data) + print(client.get_job_info()) job_data = _enrich_job_data(client.get_job_info()) + except Exception as e: error = str(e) + print(error) else: error = "OctoPrint is not configured." @@ -73,6 +83,7 @@ def get_gcode_dir(): def prepare(): from app.models import PrintFile import os + from app.utils.gcode_parser import get_gcode_metadata # Query only the sliced GCode files belonging to the current user user_files = PrintFile.query.filter_by(user_id=current_user.id, status='sliced').order_by(PrintFile.created_at.desc()).all() @@ -95,8 +106,16 @@ def prepare(): gcode_path = os.path.join(gcode_dir, gcode_filename) size = 0 + f.meta_print_time = '-' + f.meta_first_layer_time = '-' + f.meta_filament_used = '-' + if os.path.exists(gcode_path): size = os.path.getsize(gcode_path) + metadata = get_gcode_metadata(gcode_path) + f.meta_print_time = metadata.get('print_time', '-') + f.meta_first_layer_time = metadata.get('first_layer_time', '-') + f.meta_filament_used = metadata.get('filament_used', '-') # Upload to OctoPrint if not found but exists locally if client and gcode_filename not in octo_files_dict and size > 0: @@ -118,7 +137,10 @@ def prepare(): 'size': size, 'origin': 'local', 'path': gcode_filename, - 'gcodeAnalysis': analysis + 'gcodeAnalysis': analysis, + 'meta_print_time': f.meta_print_time, + 'meta_first_layer_time': f.meta_first_layer_time, + 'meta_filament_used': f.meta_filament_used }) error = None @@ -220,6 +242,8 @@ def api_command(): try: if cmd == 'home': client.home_axes() + elif cmd == 'auto_level': + client.auto_leveling() elif cmd == 'pause': client.pause_print() elif cmd == 'cancel': diff --git a/app/templates/admin/api_keys.html b/app/templates/admin/api_keys.html new file mode 100644 index 0000000..2a8450a --- /dev/null +++ b/app/templates/admin/api_keys.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} + +{% block content %} +
+

API Keys Management

+ +
+
Create New API Key
+
+
+ + +
+
+
+ + + + + + + + + + + + + {% for key in keys %} + + + + + + + + {% else %} + + + + {% endfor %} + +
IDNameAPI KeyCreated AtAction
{{ key.id }}{{ key.name }}{{ key.key }}{{ key.created_at.strftime('%Y-%m-%d %H:%M:%S') }} +
+ +
+
No API keys found.
+
+{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index 8e73daa..7083165 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -43,11 +43,23 @@ /* 提升 Accordion 折叠栏动画更平滑 */ .collapsing { transition: height 0.35s cubic-bezier(0.25, 0.8, 0.25, 1) !important; } + + /* 移动端适配 */ + @media (max-width: 767.98px) { + body { padding-top: 126px !important; } + .sidebar { top: 126px; width: 100%; border-bottom: 1px solid #ddd; box-shadow: 0 4px 6px rgba(0,0,0,.1); } + .sidebar-sticky { height: calc(100vh - 126px); } + .mobile-subnav { display: flex !important; } + } + @media (max-width: 454.98px) { + .navbar-brand { display: none !important; } + } -