fix: 修复环路检测失败 + 多项UX增强

修复:
- 环路检测正则 \s+(Onu\S+)\s+ → \s+(Onu\S+) (splitlines移除换行后尾随\s无法匹配)
- 权限中间件 Header(...) → Header(None) 避免缺失Auth头返回422而非401
- 环路检测请求超时30s→120s (SSH连接30+台OLT实测需58秒)

重构 (ssh_service.py):
- 提取 _send_and_wait 为私有方法,消除3处重复内部函数
- 添加 __enter__/__exit__ 上下文管理器支持
- 加固 execute_command prompt检测 (按行匹配<DEVICE_NAME>)
- 移除未使用的settings import
- olt.py/devices.py 调用方改用 with 语法

新功能:
- 侧边栏退出登录上方显示当前用户名和角色
- 版本号从VERSION文件自动读取 (后端/health返回,前端动态显示)
- 基于广西南宁经纬度计算日落时间,自动切换深色/浅色主题
- /api/olt/loopback-detection 响应增加raw字段便于排查

基础设施:
- CLAUDE.md 加入 .gitignore
- 新增 .claude/rules/07-remote-operations.md (远程部署操作)
- 新增 .claude/rules/08-frp-notes.md (frp隧道注意事项)
- 新增 VERSION 文件 (版本号 0.10.0)
- 新增环路检测解析测试用例 (5个)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-12 11:07:29 +08:00
parent fcfa5af614
commit e5d6d843c3
14 changed files with 309 additions and 814 deletions
+11 -25
View File
@@ -263,14 +263,11 @@ def clear_onu_port(
if body.port_id not in port_ids:
raise HTTPException(status_code=400, detail="端口不在重复记录中")
ssh = SSHService(olt.ip_address, olt.username, olt.password)
try:
ssh.connect()
ssh.clear_onu_port(body.port_id)
with SSHService(olt.ip_address, olt.username, olt.password) as ssh:
ssh.clear_onu_port(body.port_id)
except Exception as e:
raise HTTPException(status_code=500, detail=f"清除失败: {str(e)}")
finally:
ssh.close()
# 从 ports 列表移除已清除的端口
remaining = [p for p in record.ports if p["port_id"] != body.port_id]
@@ -451,10 +448,9 @@ def loopback_detection(
onu_map = {(o.olt_id, o.port_id): o for o in all_onus if o.port_id}
def check_one(olt):
ssh = SSHService(olt.ip_address, olt.username, olt.password)
try:
ssh.connect()
detection = ssh.detect_loopback()
with SSHService(olt.ip_address, olt.username, olt.password) as ssh:
detection = ssh.detect_loopback()
except Exception as e:
return {
"olt_id": olt.id,
@@ -464,8 +460,6 @@ def loopback_detection(
"has_loop": False,
"loop_interfaces": [],
}
finally:
ssh.close()
loop_interfaces = []
for iface in detection.get("interfaces", []):
@@ -488,6 +482,7 @@ def loopback_detection(
"has_loop": detection["has_loop"],
"loop_interfaces": loop_interfaces,
"error": None,
"raw": detection.get("raw", ""),
}
results_map = {}
@@ -522,10 +517,9 @@ def sync_ntp(
olts = [o for o in olts if o.region in areas] if areas else []
def sync_one(olt):
ssh = SSHService(olt.ip_address, olt.username, olt.password)
try:
ssh.connect()
ssh.sync_ntp(body.old_server, body.new_server)
with SSHService(olt.ip_address, olt.username, olt.password) as ssh:
ssh.sync_ntp(body.old_server, body.new_server)
return {
"olt_ip": olt.ip_address,
"olt_location": olt.location or olt.ip_address,
@@ -539,8 +533,6 @@ def sync_ntp(
"success": False,
"error": str(e),
}
finally:
ssh.close()
results_map = {}
with ThreadPoolExecutor(max_workers=len(olts) or 1) as executor:
@@ -574,15 +566,12 @@ def get_olt_ports(
olt = db.query(OLTDevice).filter(OLTDevice.id == olt_id).first()
if not olt:
raise HTTPException(status_code=404, detail="OLT 不存在")
ssh = SSHService(olt.ip_address, olt.username, olt.password)
try:
ssh.connect()
ports = ssh.get_olt_ports()
with SSHService(olt.ip_address, olt.username, olt.password) as ssh:
ports = ssh.get_olt_ports()
return {"ports": ports}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
ssh.close()
@router.post("/devices/{olt_id}/ports/toggle")
@@ -594,13 +583,10 @@ def toggle_olt_port(olt_id: int, body: TogglePortRequest, port_name: str, db: Se
olt = db.query(OLTDevice).filter(OLTDevice.id == olt_id).first()
if not olt:
raise HTTPException(status_code=404, detail="OLT 不存在")
ssh = SSHService(olt.ip_address, olt.username, olt.password)
try:
ssh.connect()
ssh.toggle_olt_port(port_name, body.action)
with SSHService(olt.ip_address, olt.username, olt.password) as ssh:
ssh.toggle_olt_port(port_name, body.action)
return {"message": f"端口 {port_name}{'关闭' if body.action == 'shutdown' else '开启'}"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
ssh.close()