Bläddra i källkod

add ota service

xuqiang 4 månader sedan
förälder
incheckning
a6324fea71

+ 24 - 0
services/ota/Makefile

@@ -0,0 +1,24 @@
+build:
+	pyinstaller otaserver.spec
+
+release:
+	mkdir -p ./release/otaserver
+	rm -rf ./release/otaserver/*
+	pyinstaller otaserver.spec
+	cp -f ./run.sh ./release/otaserver
+	cp -f ./install.sh ./release/otaserver
+	cp -r ./dist ./release/otaserver/bin
+	cp -f ./otaserver.service ./release/otaserver
+
+install:
+	mkdir -p /usr/local/otaserver
+	cp -r ./dist /usr/local/otaserver/bin
+	cp -f ./run.sh /usr/local/otaserver
+	cp otaserver.service  /usr/lib/systemd/system
+	systemctl enable otaserver
+	systemctl start otaserver
+
+.PHONY:clean
+clean:
+	rm -rf build
+	rm -rf dist

+ 332 - 0
services/ota/app.py

@@ -0,0 +1,332 @@
+from gevent import pywsgi
+import subprocess
+import threading
+import time
+from flask import Flask, render_template, Response, jsonify, request, send_file
+from queue import Queue, Empty
+import os, sys
+import json
+import uuid
+import io
+import zipfile
+from datetime import datetime
+
+app = Flask(__name__)
+
+# 获取当前脚本所在目录(即打包后的exe所在目录)
+if getattr(sys, 'frozen', False):
+    # 如果是PyInstaller打包后的exe
+    BASE_DIR = sys._MEIPASS
+else:
+    # 普通Python脚本
+    BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+
+# 配置
+UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
+SCRIPT_FOLDER = os.path.join(BASE_DIR, 'scripts')
+# UPLOAD_FOLDER = 'uploads'
+# SCRIPT_FOLDER = 'scripts'
+
+ALLOWED_EXTENSIONS = {'tar.gz', 'tgz'}
+LOG_FOLDERS = {
+    'network': '/home/forlinx/Desktop/workspace/TPMFM-A/logs',
+    'compute': '/home/forlinx/Desktop/workspace/dataParsing/logs',
+    'monitor': '/home/forlinx/monitor/storage/logs'
+}
+
+# 创建必要的目录
+os.makedirs(UPLOAD_FOLDER, exist_ok=True)
+os.makedirs(SCRIPT_FOLDER, exist_ok=True)
+
+# 存储执行状态和输出的全局变量
+execution_data = {
+    'is_running': False,
+    'output': [],
+    'process': None,
+    'current_file': None
+}
+
+def allowed_file(filename):
+    """检查文件扩展名是否允许"""
+    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS or \
+           filename.endswith('.tar.gz') or filename.endswith('.tgz')
+
+def run_fixed_shell_script(software_type, uploaded_file_path, output_queue):
+    """运行固定的shell脚本处理上传的文件"""
+    try:
+        # 固定的shell脚本路径
+        fixed_script_path = os.path.join(SCRIPT_FOLDER, 'run.sh')
+        
+        # 检查固定脚本是否存在,如果不存在则创建
+        if not os.path.exists(fixed_script_path):
+            create_default_script(fixed_script_path)
+        
+        # 确保脚本有执行权限
+        os.chmod(fixed_script_path, 0o755)
+        
+        # 使用Popen执行固定脚本,传入上传的文件作为参数
+        process = subprocess.Popen(
+            ['bash', fixed_script_path, software_type, uploaded_file_path],
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            text=True,
+            bufsize=1,
+            universal_newlines=True
+        )
+        
+        execution_data['process'] = process
+        
+        # 实时读取输出
+        for line in iter(process.stdout.readline, ''):
+            if line:
+                # 清理行尾的换行符,添加HTML换行
+                clean_line = line.rstrip('\n')
+                output_queue.put(f"{clean_line}<br>")
+        
+        process.wait()
+        return_code = process.returncode
+        
+        if return_code == 0:
+            output_queue.put(f"<span style='color: green'>文件处理完成!退出码: {return_code}</span><br>")
+        else:
+            output_queue.put(f"<span style='color: red'>文件处理失败!退出码: {return_code}</span><br>")
+            
+    except Exception as e:
+        output_queue.put(f"<span style='color: red'>执行错误: {str(e)}</span><br>")
+    finally:
+        execution_data['is_running'] = False
+        execution_data['process'] = None
+
+@app.route('/')
+def index():
+    """主页面"""
+    return render_template('index.html')
+
+@app.route('/upload', methods=['POST'])
+def upload_file():
+    """上传文件并自动执行处理"""
+    if execution_data['is_running']:
+        return jsonify({
+            'success': False,
+            'message': '当前有任务正在执行,请等待完成'
+        })
+    
+    # 检查是否有文件
+    if 'file' not in request.files:
+        return jsonify({'success': False, 'message': '没有选择文件'})
+    
+    file = request.files['file']
+    # 获取 software_type
+    software_type = request.form.get('software_type')
+    
+    if file.filename == '':
+        return jsonify({'success': False, 'message': '没有选择文件'})
+    
+    if file and allowed_file(file.filename):
+        # 生成唯一文件名
+        # filename = f"{uuid.uuid4().hex}_{file.filename}"
+        filename = file.filename
+        filepath = os.path.join(UPLOAD_FOLDER, filename)
+
+        # 保存文件
+        file.save(filepath)
+        
+        # 重置输出
+        execution_data['output'] = []
+        execution_data['is_running'] = True
+        execution_data['current_file'] = filename
+        
+        # 创建输出队列
+        output_queue = Queue()
+        
+        # 在新线程中执行固定的shell脚本
+        thread = threading.Thread(
+            target=run_fixed_shell_script,
+            args=(software_type, filepath, output_queue),
+            daemon=True
+        )
+        thread.start()
+        
+        # 启动一个线程来定期从队列中获取输出
+        def collect_output():
+            while execution_data['is_running'] or not output_queue.empty():
+                try:
+                    # 从队列获取输出,最多等待1秒
+                    line = output_queue.get(timeout=1)
+                    execution_data['output'].append(line)
+                except Empty:
+                    continue
+                except Exception as e:
+                    print(f"收集输出时出错: {e}")
+                    break
+        
+        collection_thread = threading.Thread(target=collect_output, daemon=True)
+        collection_thread.start()
+        
+        return jsonify({
+            'success': True,
+            'message': '文件上传成功,开始处理',
+            'filename': filename
+        })
+    
+    return jsonify({'success': False, 'message': '不支持的文件类型'})
+
+@app.route('/stream')
+def stream():
+    """SSE流式输出"""
+    def generate():
+        last_index = 0
+        while execution_data['is_running']:
+            # 检查是否有新输出
+            current_output = execution_data['output']
+            if len(current_output) > last_index:
+                # 发送新内容
+                for i in range(last_index, len(current_output)):
+                    yield f"data: {json.dumps({'output': current_output[i]})}\n\n"
+                last_index = len(current_output)
+            
+            time.sleep(0.1)  # 短暂休眠减少CPU使用
+        
+        # 发送最后可能剩余的输出
+        current_output = execution_data['output']
+        if len(current_output) > last_index:
+            for i in range(last_index, len(current_output)):
+                yield f"data: {json.dumps({'output': current_output[i]})}\n\n"
+        
+        # 发送结束标志
+        yield f"data: {json.dumps({'output': '<br><strong>处理完成</strong>'})}\n\n"
+    
+    return Response(
+        generate(),
+        mimetype='text/event-stream',
+        headers={
+            'Cache-Control': 'no-cache',
+            'X-Accel-Buffering': 'no'
+        }
+    )
+
+@app.route('/status')
+def get_status():
+    """获取执行状态"""
+    return jsonify({
+        'is_running': execution_data['is_running'],
+        'current_file': execution_data['current_file'],
+        'output_length': len(execution_data['output'])
+    })
+
+@app.route('/stop')
+def stop_execution():
+    """停止当前执行"""
+    if execution_data['is_running'] and execution_data['process']:
+        try:
+            execution_data['process'].terminate()
+            execution_data['is_running'] = False
+            return jsonify({
+                'success': True,
+                'message': '已停止处理'
+            })
+        except Exception as e:
+            return jsonify({
+                'success': False,
+                'message': f'停止失败: {str(e)}'
+            })
+    
+    return jsonify({
+        'success': False,
+        'message': '没有正在执行的任务'
+    })
+
+@app.route('/clear_output', methods=['POST'])
+def clear_output():
+    """清空输出"""
+    execution_data['output'] = []
+    return jsonify({'success': True, 'message': '输出已清空'})
+
+@app.route('/download_logs', methods=['GET'])
+def download_logs():
+    """下载指定软件的日志文件"""
+    software_type = request.args.get('software', 'network')
+    
+    if software_type not in LOG_FOLDERS:
+        return jsonify({
+            'success': False,
+            'message': '不支持的软件类型'
+        }), 400
+    
+    log_folder = LOG_FOLDERS[software_type]
+    
+    # 检查日志文件夹是否存在
+    if not os.path.exists(log_folder):
+        return jsonify({
+            'success': False,
+            'message': f'日志文件夹不存在: {log_folder}'
+        }), 404
+    
+    # 获取所有.log文件
+    log_files = []
+    for root, dirs, files in os.walk(log_folder):
+        for file in files:
+            if file.endswith('.log'):
+                log_files.append(os.path.join(root, file))
+    
+    if not log_files:
+        return jsonify({
+            'success': False,
+            'message': '未找到日志文件'
+        }), 404
+    
+    # 创建临时ZIP文件
+    temp_zip = io.BytesIO()
+    
+    try:
+        with zipfile.ZipFile(temp_zip, 'w', zipfile.ZIP_DEFLATED) as zipf:
+            for log_file in log_files:
+                # 获取相对路径
+                rel_path = os.path.relpath(log_file, log_folder)
+                zipf.write(log_file, arcname=rel_path)
+        
+        temp_zip.seek(0)
+        
+        # 设置下载文件名
+        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
+        filename = f'{software_type}_logs_{timestamp}.zip'
+        
+        # 记录下载日志
+        # download_log_path = os.path.join(log_folder, f'{software_type}_download.log')
+        # with open(download_log_path, 'a') as f:
+        #     f.write(f'[{datetime.now()}] Logs downloaded: {filename}\n')
+        
+        # 发送ZIP文件
+        return send_file(
+            temp_zip,
+            mimetype='application/zip',
+            as_attachment=True,
+            download_name=filename
+        )
+        
+    except Exception as e:
+        return jsonify({
+            'success': False,
+            'message': f'创建ZIP文件失败: {str(e)}'
+        }), 500
+
+if __name__ == '__main__':
+    # 确保固定脚本存在
+    fixed_script_path = os.path.join(SCRIPT_FOLDER, 'run.sh')
+    
+    print(f"固定脚本位置: {fixed_script_path}")
+    print(f"上传目录: {UPLOAD_FOLDER}")
+    
+    # app.run(
+    #     debug=True,
+    #     host='0.0.0.0',
+    #     port=5000,
+    #     threaded=True
+    # )
+
+    server = pywsgi.WSGIServer(('0.0.0.0', 5000), app)
+    try:
+        server.serve_forever()
+    except KeyboardInterrupt:
+        print("OTA server stopped by user")
+        server.stop()

+ 9 - 0
services/ota/install.sh

@@ -0,0 +1,9 @@
+#!/bin/bash
+
+mkdir -p /usr/local/otaserver/bin
+cp -f ./bin/otaserver /usr/local/otaserver/bin
+cp -f ./run.sh /usr/local/otaserver
+cp -f ./otaserver.service /usr/lib/systemd/system
+
+systemctl daemon-reload
+systemctl enable --now otaserver

+ 20 - 0
services/ota/otaserver.service

@@ -0,0 +1,20 @@
+[Unit]
+Description=OTA SERVER
+After=network.target
+Wants=network.target
+
+[Service]
+Type=simple
+User=root
+Group=root
+WorkingDirectory=/usr/local/otaserver
+ExecStart=bash run.sh
+Restart=always
+RestartSec=3
+
+# 如果需要日志
+StandardOutput=journal
+StandardError=journal
+
+[Install]
+WantedBy=multi-user.target

+ 38 - 0
services/ota/otaserver.spec

@@ -0,0 +1,38 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+
+a = Analysis(
+    ['app.py'],
+    pathex=[],
+    binaries=[],
+    datas=[('templates', 'templates'), ('scripts', 'scripts')],
+    hiddenimports=[],
+    hookspath=[],
+    hooksconfig={},
+    runtime_hooks=[],
+    excludes=[],
+    noarchive=False,
+    optimize=0,
+)
+pyz = PYZ(a.pure)
+
+exe = EXE(
+    pyz,
+    a.scripts,
+    a.binaries,
+    a.datas,
+    [],
+    name='otaserver',
+    debug=False,
+    bootloader_ignore_signals=False,
+    strip=False,
+    upx=True,
+    upx_exclude=[],
+    runtime_tmpdir=None,
+    console=True,
+    disable_windowed_traceback=False,
+    argv_emulation=False,
+    target_arch=None,
+    codesign_identity=None,
+    entitlements_file=None,
+)

+ 3 - 0
services/ota/requirements.txt

@@ -0,0 +1,3 @@
+Flask==3.1.2
+gevent==25.9.1
+pyinstaller==6.17.0

+ 5 - 0
services/ota/run.sh

@@ -0,0 +1,5 @@
+#/bin/bash
+
+# start service
+chmod +x /usr/local/otaserver/bin/otaserver
+/usr/local/otaserver/bin/otaserver

+ 80 - 0
services/ota/scripts/run.sh

@@ -0,0 +1,80 @@
+#!/bin/bash
+
+type=$1
+filename=$2
+
+if [ -z "$type" ] || [ -z "$filename" ]; then
+    echo "Usage: $0 <type> <filename>"
+    exit 1
+fi
+
+dirpath=$(dirname "$filename")
+echo $type $filename $dirpath
+
+network_dest_dir="/home/forlinx/Desktop/workspace/TPMFM-A"
+monitor_dest_dir="/home/forlinx/monitor"
+compute_dest_dir="/home/forlinx/Desktop/workspace/dataParsing"
+
+mkdir -p "$network_dest_dir" "$monitor_dest_dir" "$compute_dest_dir"
+
+echo "stopping app..."
+systemctl stop monitor
+echo "Waiting for monitor to fully stop..."
+sleep 2  # 等待 2 秒
+while systemctl is-active --quiet monitor; do
+    echo "monitor still running, waiting..."
+    sleep 1
+done
+echo "monitor stopped."
+
+# 根据类型解压
+case "$type" in
+    network)
+        echo "Extracting network package..."
+        tar --overwrite -zxvf "$filename" -C "$network_dest_dir"
+        ;;
+    monitor)
+        echo "Extracting monitor package..."
+        tar --overwrite -zxvf "$filename" -C "$monitor_dest_dir"
+        if [ -f $monitor_dest_dir/monitor.service ]; then
+            cp "$monitor_dest_dir/monitor.service" /etc/systemd/system/
+            systemctl daemon-reload
+        else
+            echo "monitor.service not found in $monitor_dest_dir"
+        fi
+        ;;
+    compute)
+        echo "Extracting compute package..."
+        tar --overwrite -zxvf "$filename" -C "$compute_dest_dir"
+        ;;
+    *)
+        echo "Unsupported file type: $type"
+        exit 1
+        ;;
+esac
+
+echo "restarting app..."
+systemctl enable --now monitor
+
+
+echo "begin cleanup..."
+if [ -d "$dirpath" ]; then
+    rm -rf "$dirpath"/*
+else
+    echo "Directory $dirpath does not exist, skipping cleanup."
+fi
+echo "cleanup complete"
+
+# 再次检查 monitor 服务状态
+status=$(systemctl is-active monitor)
+echo "Monitor service status after restart: $status"
+
+
+sleep 3
+# 可选:显示详细状态
+# systemctl status monitor
+journalctl -u monitor.service -n 100
+
+
+echo ""
+echo "脚本执行完成!"

+ 1210 - 0
services/ota/templates/index.html

@@ -0,0 +1,1210 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>软件更新管理系统</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+        
+        body {
+            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            min-height: 100vh;
+            padding: 20px;
+        }
+        
+        .container {
+            max-width: 1200px;
+            margin: 0 auto;
+            background: white;
+            border-radius: 15px;
+            box-shadow: 0 20px 40px rgba(0,0,0,0.1);
+            overflow: hidden;
+        }
+        
+        .header {
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: white;
+            padding: 30px;
+            text-align: center;
+        }
+        
+        .header h1 {
+            font-size: 2.5em;
+            margin-bottom: 10px;
+        }
+        
+        .content {
+            padding: 30px;
+        }
+        
+        .upload-section {
+            background: #f8f9fa;
+            border-radius: 10px;
+            padding: 25px;
+            margin-bottom: 20px;
+            box-shadow: 0 5px 15px rgba(0,0,0,0.05);
+            border-left: 5px solid;
+        }
+        
+        .network-section {
+            border-left-color: #667eea;
+        }
+        
+        .compute-section {
+            border-left-color: #00b09b;
+        }
+        
+        .monitor-section {
+            border-left-color: #ff416c;
+        }
+        
+        .upload-section h2 {
+            color: #333;
+            margin-bottom: 20px;
+            padding-bottom: 10px;
+            border-bottom: 2px solid;
+        }
+        
+        .network-section h2 {
+            border-bottom-color: #667eea;
+        }
+        
+        .compute-section h2 {
+            border-bottom-color: #00b09b;
+        }
+        
+        .monitor-section h2 {
+            border-bottom-color: #ff416c;
+        }
+        
+        .section-header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin-bottom: 15px;
+        }
+        
+        .section-tag {
+            padding: 4px 12px;
+            border-radius: 20px;
+            font-size: 0.8em;
+            font-weight: 600;
+            color: white;
+        }
+        
+        .network-tag {
+            background: #667eea;
+        }
+        
+        .compute-tag {
+            background: #00b09b;
+        }
+        
+        .monitor-tag {
+            background: #ff416c;
+        }
+        
+        .file-input-container {
+            margin-bottom: 20px;
+        }
+        
+        .file-input-label {
+            display: inline-block;
+            padding: 12px 24px;
+            color: white;
+            border-radius: 6px;
+            cursor: pointer;
+            font-weight: 600;
+            transition: all 0.3s ease;
+        }
+        
+        .network-upload-btn {
+            background: #667eea;
+        }
+        
+        .compute-upload-btn {
+            background: #00b09b;
+        }
+        
+        .monitor-upload-btn {
+            background: #ff416c;
+        }
+        
+        .file-input-label:hover {
+            transform: translateY(-2px);
+            box-shadow: 0 5px 15px rgba(0,0,0,0.1);
+        }
+        
+        .network-upload-btn:hover {
+            background: #5a6fd8;
+        }
+        
+        .compute-upload-btn:hover {
+            background: #009688;
+        }
+        
+        .monitor-upload-btn:hover {
+            background: #ff2b55;
+        }
+        
+        .file-input {
+            display: none;
+        }
+        
+        .file-name {
+            margin-left: 15px;
+            color: #666;
+            font-style: italic;
+        }
+        
+        .selected-file {
+            margin-top: 15px;
+            padding: 15px;
+            background: #e9ecef;
+            border-radius: 8px;
+            border-left: 4px solid;
+        }
+        
+        .network-file {
+            border-left-color: #667eea;
+        }
+        
+        .compute-file {
+            border-left-color: #00b09b;
+        }
+        
+        .monitor-file {
+            border-left-color: #ff416c;
+        }
+        
+        .selected-file h4 {
+            margin-bottom: 5px;
+            color: #333;
+        }
+        
+        .selected-file p {
+            color: #666;
+            font-size: 0.9em;
+        }
+        
+        .output-container {
+            height: 400px;
+            overflow-y: auto;
+            background: #1e1e1e;
+            border-radius: 8px;
+            padding: 20px;
+            font-family: 'Consolas', 'Monaco', monospace;
+            font-size: 14px;
+            color: #d4d4d4;
+            line-height: 1.5;
+        }
+        
+        .output-container::-webkit-scrollbar {
+            width: 8px;
+        }
+        
+        .output-container::-webkit-scrollbar-track {
+            background: #2d2d2d;
+        }
+        
+        .output-container::-webkit-scrollbar-thumb {
+            background: #555;
+            border-radius: 4px;
+        }
+        
+        .button-group {
+            display: flex;
+            gap: 10px;
+            margin-top: 20px;
+            flex-wrap: wrap;
+        }
+        
+        button {
+            padding: 12px 24px;
+            border: none;
+            border-radius: 6px;
+            cursor: pointer;
+            font-weight: 600;
+            transition: all 0.3s ease;
+            font-size: 16px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: 8px;
+        }
+        
+        .update-btn {
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: white;
+        }
+        
+        .compute-update-btn {
+            background: linear-gradient(135deg, #00b09b 0%, #96c93d 100%);
+            color: white;
+        }
+        
+        .monitor-update-btn {
+            background: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%);
+            color: white;
+        }
+        
+        .stop-btn {
+            background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
+            color: white;
+        }
+        
+        .clear-btn {
+            background: linear-gradient(135deg, #17a2b8 0%, #138496 100%);
+            color: white;
+        }
+        
+        .log-btn {
+            background: linear-gradient(135deg, #ff9800 0%, #ff5722 100%);
+            color: white;
+        }
+        
+        .compute-log-btn {
+            background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);
+            color: white;
+        }
+        
+        .monitor-log-btn {
+            background: linear-gradient(135deg, #9c27b0 0%, #6a1b9a 100%);
+            color: white;
+        }
+        
+        button:hover:not(:disabled) {
+            transform: translateY(-2px);
+            box-shadow: 0 7px 14px rgba(0,0,0,0.1);
+        }
+        
+        button:disabled {
+            opacity: 0.6;
+            cursor: not-allowed;
+        }
+        
+        .status-indicator {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            padding: 10px 20px;
+            border-radius: 20px;
+            font-weight: 600;
+            margin-bottom: 20px;
+            background: #f8f9fa;
+            border: 1px solid #dee2e6;
+        }
+        
+        .status-running {
+            background: #d4edda;
+            border-color: #c3e6cb;
+            color: #155724;
+        }
+        
+        .status-stopped {
+            background: #f8d7da;
+            border-color: #f5c6cb;
+            color: #721c24;
+        }
+        
+        .status-idle {
+            background: #fff3cd;
+            border-color: #ffeeba;
+            color: #856404;
+        }
+        
+        .status-dot {
+            width: 12px;
+            height: 12px;
+            border-radius: 50%;
+            animation: pulse 2s infinite;
+        }
+        
+        .running-dot {
+            background: #28a745;
+        }
+        
+        .stopped-dot {
+            background: #dc3545;
+        }
+        
+        .idle-dot {
+            background: #ffc107;
+        }
+        
+        @keyframes pulse {
+            0% { opacity: 1; }
+            50% { opacity: 0.5; }
+            100% { opacity: 1; }
+        }
+        
+        .loading {
+            display: inline-block;
+            width: 20px;
+            height: 20px;
+            border: 3px solid #f3f3f3;
+            border-top: 3px solid;
+            border-radius: 50%;
+            animation: spin 1s linear infinite;
+        }
+        
+        .network-loading {
+            border-top-color: #667eea;
+        }
+        
+        .compute-loading {
+            border-top-color: #00b09b;
+        }
+        
+        .monitor-loading {
+            border-top-color: #ff416c;
+        }
+        
+        @keyframes spin {
+            0% { transform: rotate(0deg); }
+            100% { transform: rotate(360deg); }
+        }
+        
+        .info-box {
+            background: #e7f3ff;
+            border-radius: 8px;
+            padding: 15px;
+            margin-bottom: 20px;
+            border-left: 4px solid #2196F3;
+        }
+        
+        .info-box h3 {
+            color: #1976D2;
+            margin-bottom: 10px;
+        }
+        
+        .info-box ul {
+            padding-left: 20px;
+            color: #555;
+        }
+        
+        .info-box li {
+            margin-bottom: 5px;
+        }
+        
+        .upload-icon {
+            font-size: 1.2em;
+        }
+        
+        .software-grid {
+            display: grid;
+            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+            gap: 20px;
+            margin-bottom: 30px;
+        }
+        
+        .software-card {
+            background: white;
+            border-radius: 10px;
+            padding: 20px;
+            box-shadow: 0 5px 15px rgba(0,0,0,0.05);
+            border: 1px solid #dee2e6;
+        }
+        
+        .software-card h3 {
+            color: #333;
+            margin-bottom: 15px;
+            padding-bottom: 10px;
+            border-bottom: 2px solid;
+        }
+        
+        .network-card h3 {
+            border-bottom-color: #667eea;
+        }
+        
+        .compute-card h3 {
+            border-bottom-color: #00b09b;
+        }
+        
+        .monitor-card h3 {
+            border-bottom-color: #ff416c;
+        }
+        
+        .software-info {
+            margin-bottom: 15px;
+            color: #666;
+            font-size: 0.9em;
+        }
+        
+        .current-version {
+            background: #e9ecef;
+            padding: 8px 12px;
+            border-radius: 4px;
+            font-family: monospace;
+            margin-top: 10px;
+        }
+        
+        .tabs {
+            display: flex;
+            border-bottom: 1px solid #dee2e6;
+            margin-bottom: 20px;
+        }
+        
+        .tab {
+            padding: 10px 20px;
+            cursor: pointer;
+            border-bottom: 3px solid transparent;
+            transition: all 0.3s ease;
+        }
+        
+        .tab:hover {
+            background: #f8f9fa;
+        }
+        
+        .tab.active {
+            border-bottom-color: #667eea;
+            color: #667eea;
+            font-weight: 600;
+        }
+        
+        .tab-content {
+            display: none;
+        }
+        
+        .tab-content.active {
+            display: block;
+        }
+        
+        .log-info {
+            background: #fff3cd;
+            border-left: 4px solid #ffc107;
+            padding: 12px 15px;
+            margin-top: 15px;
+            border-radius: 6px;
+            font-size: 0.9em;
+        }
+        
+        .log-info h4 {
+            color: #856404;
+            margin-bottom: 5px;
+        }
+        
+        .log-info ul {
+            padding-left: 20px;
+            color: #856404;
+        }
+        
+        .notification {
+            position: fixed;
+            top: 20px;
+            right: 20px;
+            padding: 15px 20px;
+            background: white;
+            border-radius: 8px;
+            box-shadow: 0 5px 20px rgba(0,0,0,0.2);
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            z-index: 1000;
+            animation: slideIn 0.3s ease;
+            border-left: 4px solid;
+        }
+        
+        .notification-success {
+            border-left-color: #28a745;
+        }
+        
+        .notification-error {
+            border-left-color: #dc3545;
+        }
+        
+        .notification-info {
+            border-left-color: #17a2b8;
+        }
+        
+        @keyframes slideIn {
+            from {
+                transform: translateX(100%);
+                opacity: 0;
+            }
+            to {
+                transform: translateX(0);
+                opacity: 1;
+            }
+        }
+        
+        .close-notification {
+            background: none;
+            border: none;
+            font-size: 1.2em;
+            cursor: pointer;
+            color: #666;
+            padding: 0;
+            width: 24px;
+            height: 24px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+        }
+        
+        .close-notification:hover {
+            color: #333;
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <div class="header">
+            <h1>软件更新管理系统</h1>
+            <p>统一管理网络解析、计算、画面监控软件的更新和日志</p>
+        </div>
+        
+        <div class="content">
+            <div id="statusIndicator" class="status-indicator status-idle">
+                <span class="status-dot idle-dot"></span>
+                <span id="statusText">就绪 - 等待操作</span>
+                <span id="currentFile" style="margin-left: auto; font-weight: normal; color: #666;"></span>
+            </div>
+            
+            <div class="info-box">
+                <h3>使用说明</h3>
+                <ul>
+                    <li>选择对应软件类型的更新文件,点击更新按钮</li>
+                    <li>支持的文件类型: .tar.gz, .tgz</li>
+                    <li>处理过程实时显示在下方输出区域</li>
+                    <li>不同软件使用不同的颜色标识,便于区分</li>
+                    <li>点击"下载日志"按钮可以下载软件的所有日志文件</li>
+                </ul>
+            </div>
+            
+            <div class="tabs">
+                <div class="tab active" data-tab="network">网络解析软件</div>
+                <div class="tab" data-tab="compute">计算软件</div>
+                <div class="tab" data-tab="monitor">画面监控软件</div>
+            </div>
+            
+            <!-- 网络解析软件更新 -->
+            <div id="network-tab" class="tab-content active">
+                <div class="upload-section network-section">
+                    <h2>网络解析软件更新</h2>
+                    <div class="file-input-container">
+                        <label for="networkFileInput" class="file-input-label network-upload-btn">
+                            <span class="upload-icon">📁</span> 选择网络软件更新文件
+                        </label>
+                        <input type="file" id="networkFileInput" class="file-input" accept=".tar.gz,.tgz">
+                        <span id="networkFileName" class="file-name">未选择文件</span>
+                    </div>
+                    
+                    <div id="networkFileInfo" class="selected-file network-file" style="display: none;">
+                        <h4>已选择网络软件文件:</h4>
+                        <p id="networkFileInfoText"></p>
+                    </div>
+                    
+                    <div class="log-info">
+                        <h4>日志文件说明:</h4>
+                        <ul>
+                            <li>支持下载所有.log文件为ZIP包</li>
+                        </ul>
+                    </div>
+                    
+                    <div class="button-group">
+                        <button id="networkUpdateBtn" class="update-btn" disabled>
+                            <span>更新网络软件</span>
+                        </button>
+                        <button id="stopBtn" class="stop-btn" disabled>
+                            <span>🛑</span> 停止处理
+                        </button>
+                        <button id="clearBtn" class="clear-btn">
+                            <span>🗑️</span> 清空输出
+                        </button>
+                        <button id="networkLogBtn" class="log-btn">
+                            <span>📋</span> 下载日志
+                        </button>
+                    </div>
+                </div>
+            </div>
+            
+            <!-- 计算软件更新 -->
+            <div id="compute-tab" class="tab-content">
+                <div class="upload-section compute-section">
+                    <h2>计算软件更新</h2>
+                    <div class="file-input-container">
+                        <label for="computeFileInput" class="file-input-label compute-upload-btn">
+                            <span class="upload-icon">📁</span> 选择计算软件更新文件
+                        </label>
+                        <input type="file" id="computeFileInput" class="file-input" accept=".tar.gz,.tgz">
+                        <span id="computeFileName" class="file-name">未选择文件</span>
+                    </div>
+                    
+                    <div id="computeFileInfo" class="selected-file compute-file" style="display: none;">
+                        <h4>已选择计算软件文件:</h4>
+                        <p id="computeFileInfoText"></p>
+                    </div>
+                    
+                    <div class="log-info">
+                        <h4>日志文件说明:</h4>
+                        <ul>
+                            <li>支持下载所有.log文件为ZIP包</li>
+                        </ul>
+                    </div>
+                    
+                    <div class="button-group">
+                        <button id="computeUpdateBtn" class="compute-update-btn" disabled>
+                            <span>更新计算软件</span>
+                        </button>
+                        <button id="stopBtnCompute" class="stop-btn" disabled>
+                            <span>🛑</span> 停止处理
+                        </button>
+                        <button id="clearBtnCompute" class="clear-btn">
+                            <span>🗑️</span> 清空输出
+                        </button>
+                        <button id="computeLogBtn" class="compute-log-btn">
+                            <span>📋</span> 下载日志
+                        </button>
+                    </div>
+                </div>
+            </div>
+            
+            <!-- 画面监控软件更新 -->
+            <div id="monitor-tab" class="tab-content">
+                <div class="upload-section monitor-section">
+                    <h2>画面监控软件更新</h2>
+                    <div class="file-input-container">
+                        <label for="monitorFileInput" class="file-input-label monitor-upload-btn">
+                            <span class="upload-icon">📁</span> 选择监控软件更新文件
+                        </label>
+                        <input type="file" id="monitorFileInput" class="file-input" accept=".tar.gz,.tgz">
+                        <span id="monitorFileName" class="file-name">未选择文件</span>
+                    </div>
+                    
+                    <div id="monitorFileInfo" class="selected-file monitor-file" style="display: none;">
+                        <h4>已选择监控软件文件:</h4>
+                        <p id="monitorFileInfoText"></p>
+                    </div>
+                    
+                    <div class="log-info">
+                        <h4>日志文件说明:</h4>
+                        <ul>
+                            <li>支持下载所有.log文件为ZIP包</li>
+                        </ul>
+                    </div>
+                    
+                    <div class="button-group">
+                        <button id="monitorUpdateBtn" class="monitor-update-btn" disabled>
+                            <span>更新监控软件</span>
+                        </button>
+                        <button id="stopBtnMonitor" class="stop-btn" disabled>
+                            <span>🛑</span> 停止处理
+                        </button>
+                        <button id="clearBtnMonitor" class="clear-btn">
+                            <span>🗑️</span> 清空输出
+                        </button>
+                        <button id="monitorLogBtn" class="monitor-log-btn">
+                            <span>📋</span> 下载日志
+                        </button>
+                    </div>
+                </div>
+            </div>
+            
+            <div class="upload-section">
+                <h2>执行输出</h2>
+                <div id="output" class="output-container">
+                    选择软件更新文件并点击更新按钮开始处理...
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div id="notificationContainer" style="display: none;"></div>
+
+    <script>
+        let selectedFiles = {
+            network: null,
+            compute: null,
+            monitor: null
+        };
+        
+        let isRunning = false;
+        let eventSource = null;
+        let uploadInProgress = false;
+        let currentSoftware = 'network';
+
+        // 初始化
+        document.addEventListener('DOMContentLoaded', function() {
+            // 标签页切换
+            document.querySelectorAll('.tab').forEach(tab => {
+                tab.addEventListener('click', () => {
+                    const tabId = tab.dataset.tab;
+                    switchTab(tabId);
+                });
+            });
+
+            // 网络软件事件监听
+            document.getElementById('networkFileInput').addEventListener('change', (e) => handleFileSelect(e, 'network'));
+            document.getElementById('networkUpdateBtn').addEventListener('click', () => uploadAndExecute('network'));
+            document.getElementById('networkLogBtn').addEventListener('click', () => downloadLogs('network'));
+            
+            // 计算软件事件监听
+            document.getElementById('computeFileInput').addEventListener('change', (e) => handleFileSelect(e, 'compute'));
+            document.getElementById('computeUpdateBtn').addEventListener('click', () => uploadAndExecute('compute'));
+            document.getElementById('computeLogBtn').addEventListener('click', () => downloadLogs('compute'));
+            
+            // 监控软件事件监听
+            document.getElementById('monitorFileInput').addEventListener('change', (e) => handleFileSelect(e, 'monitor'));
+            document.getElementById('monitorUpdateBtn').addEventListener('click', () => uploadAndExecute('monitor'));
+            document.getElementById('monitorLogBtn').addEventListener('click', () => downloadLogs('monitor'));
+
+            // 停止按钮事件监听(共用)
+            document.getElementById('stopBtn').addEventListener('click', stopExecution);
+            document.getElementById('stopBtnCompute').addEventListener('click', stopExecution);
+            document.getElementById('stopBtnMonitor').addEventListener('click', stopExecution);
+            
+            // 清空按钮事件监听
+            document.getElementById('clearBtn').addEventListener('click', clearOutput);
+            document.getElementById('clearBtnCompute').addEventListener('click', clearOutput);
+            document.getElementById('clearBtnMonitor').addEventListener('click', clearOutput);
+            
+            // 检查初始状态
+            checkStatus();
+        });
+
+        // 切换标签页
+        function switchTab(tabId) {
+            // 更新标签页
+            document.querySelectorAll('.tab').forEach(tab => {
+                tab.classList.remove('active');
+            });
+            document.querySelector(`.tab[data-tab="${tabId}"]`).classList.add('active');
+            
+            // 更新内容
+            document.querySelectorAll('.tab-content').forEach(content => {
+                content.classList.remove('active');
+            });
+            document.getElementById(`${tabId}-tab`).classList.add('active');
+            
+            currentSoftware = tabId;
+        }
+
+        // 处理文件选择
+        function handleFileSelect(event, softwareType) {
+            const file = event.target.files[0];
+            if (file) {
+                selectedFiles[softwareType] = file;
+                
+                // 更新UI
+                document.getElementById(`${softwareType}FileName`).textContent = file.name;
+                document.getElementById(`${softwareType}FileInfoText`).textContent = 
+                    `${file.name} (${formatFileSize(file.size)})`;
+                document.getElementById(`${softwareType}FileInfo`).style.display = 'block';
+                document.getElementById(`${softwareType}UpdateBtn`).disabled = false;
+                
+                updateStatus('idle', `已选择${getSoftwareName(softwareType)}文件: ${file.name}`);
+            }
+        }
+
+        // 获取软件名称
+        function getSoftwareName(softwareType) {
+            const names = {
+                network: '网络解析软件',
+                compute: '计算软件',
+                monitor: '画面监控软件'
+            };
+            return names[softwareType] || '软件';
+        }
+
+        // 格式化文件大小
+        function formatFileSize(bytes) {
+            if (bytes === 0) return '0 Bytes';
+            const k = 1024;
+            const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+            const i = Math.floor(Math.log(bytes) / Math.log(k));
+            return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+        }
+
+        // 下载日志文件
+        // 下载日志文件
+        async function downloadLogs(softwareType) {
+            try {
+                // 显示加载状态
+                const logBtn = document.getElementById(`${softwareType}LogBtn`);
+                const originalText = logBtn.innerHTML;
+                logBtn.innerHTML = '<span class="loading"></span> 正在下载...';
+                logBtn.disabled = true;
+                
+                // 发送下载请求
+                const response = await fetch(`/download_logs?software=${softwareType}`, {
+                    method: 'GET'
+                });
+                
+                if (response.ok) {
+                    // 检查响应类型
+                    const contentType = response.headers.get('content-type');
+                    
+                    if (contentType && contentType.includes('application/zip')) {
+                        // 获取文件名
+                        const contentDisposition = response.headers.get('content-disposition');
+                        let filename = `${softwareType}_logs.zip`;
+                        
+                        if (contentDisposition) {
+                            const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/i.exec(contentDisposition);
+                            if (matches && matches[1]) {
+                                filename = matches[1].replace(/['"]/g, '');
+                            }
+                        }
+                        
+                        // 创建blob并下载
+                        const blob = await response.blob();
+                        
+                        // 检查blob是否有效
+                        if (blob.size === 0) {
+                            throw new Error('下载的文件为空');
+                        }
+                        
+                        const url = window.URL.createObjectURL(blob);
+                        const a = document.createElement('a');
+                        a.href = url;
+                        a.download = filename;
+                        document.body.appendChild(a);
+                        a.click();
+                        
+                        // 清理
+                        setTimeout(() => {
+                            document.body.removeChild(a);
+                            window.URL.revokeObjectURL(url);
+                        }, 100);
+                        
+                        // 显示成功通知
+                        showNotification(`${getSoftwareName(softwareType)}日志下载成功`, 'success');
+                        updateStatus('idle', `${getSoftwareName(softwareType)}日志已下载`);
+                        
+                    } else {
+                        // 尝试解析JSON错误响应
+                        try {
+                            const errorData = await response.json();
+                            throw new Error(errorData.message || '下载失败:服务器返回错误');
+                        } catch (jsonError) {
+                            // 如果不是JSON,可能是HTML错误页面
+                            const text = await response.text();
+                            if (text.includes('<!doctype') || text.includes('<html')) {
+                                throw new Error('服务器错误:返回了HTML页面而非文件');
+                            } else {
+                                throw new Error(`下载失败:${text.substring(0, 100)}`);
+                            }
+                        }
+                    }
+                } else {
+                    // 处理HTTP错误
+                    let errorMessage = `HTTP错误: ${response.status}`;
+                    
+                    try {
+                        const errorData = await response.json();
+                        errorMessage = errorData.message || errorMessage;
+                    } catch (e) {
+                        // 如果不是JSON,尝试获取文本
+                        const text = await response.text();
+                        if (text) {
+                            errorMessage = `服务器错误: ${text.substring(0, 200)}`;
+                        }
+                    }
+                    
+                    throw new Error(errorMessage);
+                }
+            } catch (error) {
+                console.error('下载日志失败:', error);
+                
+                // 更详细的错误处理
+                let userMessage = `下载失败: ${error.message}`;
+                
+                if (error.message.includes('Network Error') || error.message.includes('Failed to fetch')) {
+                    userMessage = '网络连接失败,请检查服务器状态';
+                } else if (error.message.includes('HTML')) {
+                    userMessage = '服务器配置错误,请检查后端服务';
+                }
+                
+                showNotification(userMessage, 'error');
+                updateStatus('idle', '日志下载失败');
+                
+                // 显示调试信息(仅开发环境)
+                if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
+                    console.debug('下载失败详情:', {
+                        softwareType,
+                        error: error.message,
+                        stack: error.stack
+                    });
+                }
+            } finally {
+                // 恢复按钮状态
+                const logBtn = document.getElementById(`${softwareType}LogBtn`);
+                logBtn.innerHTML = '<span>📋</span> 下载日志';
+                logBtn.disabled = false;
+            }
+        }
+
+        // 显示通知
+        function showNotification(message, type = 'info') {
+            const notificationContainer = document.getElementById('notificationContainer');
+            
+            const notification = document.createElement('div');
+            notification.className = `notification notification-${type}`;
+            
+            const messageSpan = document.createElement('span');
+            messageSpan.textContent = message;
+            
+            const closeBtn = document.createElement('button');
+            closeBtn.className = 'close-notification';
+            closeBtn.innerHTML = '×';
+            closeBtn.onclick = () => notification.remove();
+            
+            notification.appendChild(messageSpan);
+            notification.appendChild(closeBtn);
+            
+            notificationContainer.appendChild(notification);
+            notificationContainer.style.display = 'block';
+            
+            // 3秒后自动移除
+            setTimeout(() => {
+                if (notification.parentNode) {
+                    notification.remove();
+                }
+                if (notificationContainer.children.length === 0) {
+                    notificationContainer.style.display = 'none';
+                }
+            }, 3000);
+        }
+
+        // 上传并执行
+        async function uploadAndExecute(softwareType) {
+            const file = selectedFiles[softwareType];
+            if (!file || isRunning || uploadInProgress) return;
+            
+            uploadInProgress = true;
+            currentSoftware = softwareType;
+            updateStatus('running', `正在上传${getSoftwareName(softwareType)}...`);
+            
+            try {
+                const formData = new FormData();
+                formData.append('file', file);
+                formData.append('software_type', softwareType);
+                
+                const response = await fetch('/upload', {
+                    method: 'POST',
+                    body: formData
+                });
+                
+                const data = await response.json();
+                
+                if (data.success) {
+                    // 清除输出
+                    document.getElementById('output').innerHTML = '';
+                    
+                    // 开始接收SSE流
+                    startEventStream();
+                    
+                    // 更新按钮状态
+                    document.getElementById(`${softwareType}UpdateBtn`).disabled = true;
+                    document.getElementById(`${softwareType}LogBtn`).disabled = true;
+                    document.getElementById('stopBtn').disabled = false;
+                    document.getElementById('stopBtnCompute').disabled = false;
+                    document.getElementById('stopBtnMonitor').disabled = false;
+                    
+                    updateStatus('running', `正在处理${getSoftwareName(softwareType)}: ${data.filename}`);
+                } else {
+                    alert(data.message);
+                    updateStatus('idle', `${getSoftwareName(softwareType)}上传失败`);
+                }
+            } catch (error) {
+                console.error('上传失败:', error);
+                alert('上传失败: ' + error.message);
+                updateStatus('idle', '上传失败');
+            } finally {
+                uploadInProgress = false;
+            }
+        }
+
+        // 开始事件流
+        function startEventStream() {
+            if (eventSource) {
+                eventSource.close();
+            }
+            
+            eventSource = new EventSource('/stream');
+            
+            eventSource.onmessage = function(event) {
+                const data = JSON.parse(event.data);
+                const outputDiv = document.getElementById('output');
+                
+                // 根据软件类型添加颜色标识
+                let outputHtml = '';
+                if (data.output) {
+                    const color = getSoftwareColor(currentSoftware);
+                    outputHtml = `<span style="color: ${color}">[${getSoftwareName(currentSoftware)}] </span>${data.output}`;
+                } else {
+                    outputHtml = data.output;
+                }
+                
+                outputDiv.innerHTML += outputHtml;
+                
+                // 自动滚动到底部
+                outputDiv.scrollTop = outputDiv.scrollHeight;
+                
+                // 检查是否结束
+                if (data.output && data.output.includes('处理完成')) {
+                    stopEventStream();
+                    isRunning = false;
+                    updateStatus('idle', '处理完成');
+                    
+                    // 重新启用当前软件的更新按钮和日志按钮
+                    document.getElementById(`${currentSoftware}UpdateBtn`).disabled = false;
+                    document.getElementById(`${currentSoftware}LogBtn`).disabled = false;
+                    document.getElementById('stopBtn').disabled = true;
+                    document.getElementById('stopBtnCompute').disabled = true;
+                    document.getElementById('stopBtnMonitor').disabled = true;
+                    
+                    showNotification(`${getSoftwareName(currentSoftware)}更新完成`, 'success');
+                }
+            };
+            
+            eventSource.onerror = function(error) {
+                console.error('EventSource错误:', error);
+                stopEventStream();
+                updateStatus('idle', '连接错误');
+                
+                // 重新启用按钮
+                document.getElementById(`${currentSoftware}UpdateBtn`).disabled = false;
+                document.getElementById(`${currentSoftware}LogBtn`).disabled = false;
+                document.getElementById('stopBtn').disabled = true;
+                document.getElementById('stopBtnCompute').disabled = true;
+                document.getElementById('stopBtnMonitor').disabled = true;
+                
+                showNotification('连接中断,处理可能未完成', 'error');
+            };
+        }
+
+        // 获取软件颜色
+        function getSoftwareColor(softwareType) {
+            const colors = {
+                network: '#667eea',
+                compute: '#00b09b',
+                monitor: '#ff416c'
+            };
+            return colors[softwareType] || '#d4d4d4';
+        }
+
+        // 停止事件流
+        function stopEventStream() {
+            if (eventSource) {
+                eventSource.close();
+                eventSource = null;
+            }
+        }
+
+        // 停止执行
+        async function stopExecution() {
+            if (!isRunning) return;
+            
+            try {
+                const response = await fetch('/stop');
+                const data = await response.json();
+                
+                if (data.success) {
+                    isRunning = false;
+                    updateStatus('idle', '已停止');
+                    stopEventStream();
+                    
+                    // 更新按钮状态
+                    document.getElementById(`${currentSoftware}UpdateBtn`).disabled = false;
+                    document.getElementById(`${currentSoftware}LogBtn`).disabled = false;
+                    document.getElementById('stopBtn').disabled = true;
+                    document.getElementById('stopBtnCompute').disabled = true;
+                    document.getElementById('stopBtnMonitor').disabled = true;
+                    
+                    const color = getSoftwareColor(currentSoftware);
+                    document.getElementById('output').innerHTML += 
+                        `<br><span style="color: ${color}">[${getSoftwareName(currentSoftware)}] </span>` +
+                        '<span style="color: orange;">用户手动停止处理</span><br>';
+                    
+                    showNotification('处理已停止', 'info');
+                }
+            } catch (error) {
+                console.error('停止失败:', error);
+                showNotification('停止失败', 'error');
+            }
+        }
+
+        // 清空输出
+        async function clearOutput() {
+            try {
+                const response = await fetch('/clear_output', {
+                    method: 'POST'
+                });
+                const data = await response.json();
+                
+                if (data.success) {
+                    document.getElementById('output').innerHTML = '输出已清空<br>';
+                    showNotification('输出已清空', 'info');
+                }
+            } catch (error) {
+                console.error('清空输出失败:', error);
+                showNotification('清空输出失败', 'error');
+            }
+        }
+
+        // 检查状态
+        async function checkStatus() {
+            try {
+                const response = await fetch('/status');
+                const data = await response.json();
+                
+                if (data.is_running) {
+                    isRunning = true;
+                    updateStatus('running', '正在处理中...');
+                    
+                    // 根据服务器返回的软件类型禁用对应按钮
+                    const runningSoftware = data.software_type || 'network';
+                    document.getElementById(`${runningSoftware}UpdateBtn`).disabled = true;
+                    document.getElementById(`${runningSoftware}LogBtn`).disabled = true;
+                    document.getElementById('stopBtn').disabled = false;
+                    document.getElementById('stopBtnCompute').disabled = false;
+                    document.getElementById('stopBtnMonitor').disabled = false;
+                    
+                    currentSoftware = runningSoftware;
+                    startEventStream();
+                } else {
+                    updateStatus('idle', '就绪 - 等待上传文件');
+                }
+            } catch (error) {
+                console.error('检查状态失败:', error);
+            }
+        }
+
+        // 更新状态指示器
+        function updateStatus(status, message) {
+            const indicator = document.getElementById('statusIndicator');
+            const statusText = document.getElementById('statusText');
+            const statusDot = indicator.querySelector('.status-dot');
+            
+            // 移除所有状态类
+            indicator.classList.remove('status-running', 'status-stopped', 'status-idle');
+            statusDot.classList.remove('running-dot', 'stopped-dot', 'idle-dot');
+            
+            // 添加新状态类
+            if (status === 'running') {
+                indicator.classList.add('status-running');
+                statusDot.classList.add('running-dot');
+                isRunning = true;
+            } else if (status === 'stopped') {
+                indicator.classList.add('status-stopped');
+                statusDot.classList.add('stopped-dot');
+                isRunning = false;
+            } else {
+                indicator.classList.add('status-idle');
+                statusDot.classList.add('idle-dot');
+                isRunning = false;
+            }
+            
+            statusText.textContent = message;
+        }
+    </script>
+</body>
+</html>

+ 6 - 0
services/ota/uninstall.sh

@@ -0,0 +1,6 @@
+#!/bin/bash
+
+systemctl disable --now otaserver.service
+rm -r /usr/local/otaserver
+rm /usr/lib/systemd/system/otaserver.service
+systemctl daemon-reload