feat: v0.9.0 新增记录日志、设备更换记录及IMC服务集成

- 新增 RecordsLog.vue 操作记录日志页面
- 新增 ReplacementRecords.vue 设备更换记录页面
- 新增 imc_service.py IMC网管系统集成服务
- 新增 datetime.js 前端日期时间工具函数
- 新增 OLT时间同步.md 文档
- 扩展 devices.py API:设备更换记录、批量操作等
- 扩展 ssh_service.py:OLT时间同步功能
- 扩展 olt.py:新增时间同步相关接口
- 更新 DeviceList.vue:增强设备列表功能
- 更新路由和导航菜单
- 将 .claude/ 和 .mcp.json 加入 .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 10:36:06 +08:00
parent eaabebbceb
commit 5aadbc78c6
24 changed files with 2830 additions and 45 deletions
+58
View File
@@ -500,6 +500,64 @@ def loopback_detection(
return [results_map[olt.id] for olt in olts]
class SyncNTPRequest(BaseModel):
old_server: str = "172.16.0.254"
new_server: str = "172.16.1.252"
@router.post("/sync-ntp")
def sync_ntp(
body: SyncNTPRequest,
db: Session = Depends(get_db),
current: dict = Depends(require_permission('olt.manage')),
):
"""对所有 OLT 并发执行 NTP 时间服务器同步"""
from app.services.ssh_service import SSHService
from concurrent.futures import ThreadPoolExecutor, as_completed
olts = db.query(OLTDevice).all()
if current.get('role') == 'area_admin' and current.get('assigned_area'):
areas = [a.strip() for a in current['assigned_area'].split(',') if a.strip()]
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)
return {
"olt_ip": olt.ip_address,
"olt_location": olt.location or olt.ip_address,
"success": True,
"error": None,
}
except Exception as e:
return {
"olt_ip": olt.ip_address,
"olt_location": olt.location or olt.ip_address,
"success": False,
"error": str(e),
}
finally:
ssh.close()
results_map = {}
with ThreadPoolExecutor(max_workers=len(olts) or 1) as executor:
futures = {executor.submit(sync_one, olt): olt.id for olt in olts}
for future in as_completed(futures):
r = future.result()
results_map[r["olt_ip"]] = r
results = [results_map[olt.ip_address] for olt in olts]
success_count = sum(1 for r in results if r["success"])
return {
"total": len(results),
"success": success_count,
"failed": len(results) - success_count,
"results": results,
}
class TogglePortRequest(BaseModel):
action: str # "shutdown" 或 "undo shutdown"