diff --git a/netx_api/ne_service_crud.py b/netx_api/ne_service_crud.py index fdeb0dc..6af493f 100644 --- a/netx_api/ne_service_crud.py +++ b/netx_api/ne_service_crud.py @@ -8,9 +8,10 @@ from fastapi import HTTPException from sqlalchemy import or_ from sqlalchemy.orm import Session -from .device_types import SUPPORTED_DEVICE_TYPES +from .device_types import LLDP_DISCOVERED_NE_SOURCE, SUPPORTED_DEVICE_TYPES, SUPPORTED_VENDORS from .models import ManagedNE from .ne_crypto import encrypt_secret +from .ne_hop_templates import default_bastion_username_template, default_hop_command_template from .ne_schemas import ( BatchAccountConfig, HopProxyConfig, @@ -21,6 +22,8 @@ from .ne_schemas import ( from .ne_service_common import ( _apply_hop_create, _apply_hop_update, + _normalize_hop_target_auth_mode, + _normalize_hop_vendor, _normalize_ip, _normalize_protocol, _normalize_vendor, @@ -167,7 +170,16 @@ def update_managed_ne(db: Session, ne_id: str, body: ManagedNeUpdate) -> Managed ) if any(k in data for k in hop_keys): _apply_hop_update(row, data) + # Filling IP on an LLDP placeholder promotes it into real inventory. + if "ip_address" in data and str(row.ip_address or "").strip(): + if str(row.source or "").strip().lower() in {LLDP_DISCOVERED_NE_SOURCE, "lldp"}: + row.source = "" row.updated_at = _now() + # Keep linked fabric node identity in sync (name / IP / vendor). + if any(k in data for k in ("ip_address", "name", "vendor", "device_type")): + from .topology_fabric_nodes import ensure_fabric_node_for_managed + + ensure_fabric_node_for_managed(db, row) db.commit() db.refresh(row) return row_to_out(row) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 6b23609..796cb28 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1388,6 +1388,24 @@ const en = { edgeDefaults: "Default edge styles", edgeDefaultsReset: "Factory reset", noNeLink: "Not linked to a managed NE", + termConnect: { + title: "Complete login", + titleCreate: "Create NE and connect", + hint: "This node is not managed or lacks credentials. Enter IP and protocol to connect (same as creating/completing an NE).", + name: "Name", + ip: "IP address", + port: "Port", + protocol: "Protocol", + username: "Username", + password: "Password", + connect: "Save and open terminal", + connecting: "Connecting…", + cancel: "Cancel", + ipRequired: "IP address is required", + userRequired: "Username is required for SSH", + passwordRequired: "Password is required for SSH", + saved: "Login details saved", + }, edgeManual: "Manual", edgeDiscovered: "Discovered", edgeStale: "Missing", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index ea3cd12..0fd15c5 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1382,6 +1382,24 @@ const zh = { edgeDefaults: "默认链路样式", edgeDefaultsReset: "恢复出厂", noNeLink: "未关联托管网元", + termConnect: { + title: "补全登录信息", + titleCreate: "新建网元并登录", + hint: "该网元尚未纳管或缺少登录信息,请填写 IP 与协议后连接(等同新建/补全网元)。", + name: "名称", + ip: "IP 地址", + port: "端口", + protocol: "协议", + username: "用户名", + password: "密码", + connect: "保存并打开终端", + connecting: "连接中…", + cancel: "取消", + ipRequired: "请填写 IP 地址", + userRequired: "SSH 请填写用户名", + passwordRequired: "SSH 请填写密码", + saved: "登录信息已保存", + }, edgeManual: "人工", edgeDiscovered: "发现", edgeStale: "未发现", diff --git a/web/src/index.css b/web/src/index.css index 5f68853..97eee66 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -6667,6 +6667,25 @@ pre { font-weight: 700; } +/* Selected link: glow + wider interaction halo (inline stroke otherwise hides RF defaults). */ +.topo-canvas .react-flow__edge.selected .react-flow__edge-interaction, +.topo-canvas .react-flow__edge.topo-edge--selected .react-flow__edge-interaction { + stroke: #2563eb !important; + stroke-width: 14px !important; + stroke-opacity: 0.28 !important; +} + +.topo-canvas .react-flow__edge.selected .react-flow__edge-path, +.topo-canvas .react-flow__edge.topo-edge--selected .react-flow__edge-path { + stroke-linecap: round; +} + +.topo-canvas .react-flow__edge.selected .react-flow__edge-text, +.topo-canvas .react-flow__edge.topo-edge--selected .react-flow__edge-text { + fill: #1d4ed8; + font-weight: 700; +} + .topo-node__icon { display: block; width: 56px; @@ -6797,31 +6816,44 @@ pre { 90deg, transparent 0%, transparent 52%, - rgba(0, 5, 12, 0.28) 70%, - rgba(0, 5, 12, 0.68) 88%, - rgba(0, 5, 12, 0.88) 100% + rgba(0, 5, 12, 0.18) 70%, + rgba(0, 5, 12, 0.48) 88%, + rgba(0, 5, 12, 0.72) 100% ), linear-gradient(180deg, rgba(0, 0, 0, 0.1) 0%, transparent 30%, transparent 72%, rgba(0, 0, 0, 0.38) 100%); } -/* Distant sun — upper-left, feeds the lit limb. */ -.login-page__sun { +/* Tiny sun flare — upper-left void. */ +.login-page__flares { + position: absolute; + inset: 0; + z-index: 4; + pointer-events: none; + overflow: hidden; +} + +.login-page__flare { position: absolute; - z-index: 0; - top: -2%; - left: -4%; - width: min(520px, 42vw); - height: min(520px, 42vw); border-radius: 50%; pointer-events: none; - background: radial-gradient( - circle at 55% 55%, - rgba(160, 205, 255, 0.26) 0%, - rgba(90, 150, 240, 0.12) 32%, - rgba(50, 110, 210, 0.04) 55%, - transparent 72% - ); - filter: blur(8px); + opacity: 0.85; +} + +.login-page__flare--a { + width: 44px; + height: 44px; + left: 7%; + top: 11%; + background: + radial-gradient( + circle at 50% 50%, + rgba(245, 250, 255, 0.92) 0%, + rgba(170, 210, 255, 0.48) 28%, + rgba(90, 150, 245, 0.14) 55%, + transparent 78% + ); + box-shadow: 0 0 16px 3px rgba(120, 180, 255, 0.2); + animation: login-flare-pulse-a 10s ease-in-out infinite alternate; } .login-planet { @@ -6834,24 +6866,6 @@ pre { animation: login-planet-enter 1.8s cubic-bezier(0.76, -0.46, 0.37, 0.99) both; } -/* Soft blue atmosphere outside the clipped ball — fades into space. */ -.login-planet::after { - content: ""; - position: absolute; - z-index: 0; - inset: -10%; - border-radius: 50%; - pointer-events: none; - background: radial-gradient( - 120% 105% at 28% 24%, - transparent 48%, - rgba(40, 100, 210, 0.38) 64%, - rgba(55, 125, 230, 0.18) 74%, - transparent 86% - ); - filter: blur(10px); -} - .login-planet__earth { position: relative; z-index: 1; @@ -6859,16 +6873,13 @@ pre { height: 100%; border-radius: 50%; overflow: hidden; - /* Feather the limb into space — no hard circular cut. */ - -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 86%, transparent 100%); - mask-image: radial-gradient(circle at 50% 50%, #000 86%, transparent 100%); - box-shadow: - -16px -18px 40px 8px rgba(60, 130, 230, 0.35), - -6px -8px 16px 2px rgba(80, 150, 240, 0.22); + /* Soft dissolve into space — no outer glow (Earth does not emit light). */ + -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 87%, transparent 100%); + mask-image: radial-gradient(circle at 50% 50%, #000 87%, transparent 100%); transform-origin: center center; } -/* Surface light falloff: bright TL → soft mid → deeper BR. */ +/* Gentle volume shade — cooler blue midtones. */ .login-planet__earth::before { content: ""; position: absolute; @@ -6878,19 +6889,16 @@ pre { pointer-events: none; background: linear-gradient( 148deg, - rgba(120, 175, 255, 0.22) 0%, - rgba(70, 130, 220, 0.1) 18%, - rgba(40, 90, 180, 0.04) 34%, - transparent 48%, - rgba(0, 12, 36, 0.16) 68%, - rgba(0, 8, 24, 0.36) 86%, - rgba(0, 4, 16, 0.5) 100% + rgba(70, 130, 210, 0.16) 0%, + rgba(45, 100, 185, 0.08) 20%, + transparent 42%, + rgba(0, 12, 36, 0.22) 72%, + rgba(0, 8, 24, 0.42) 100% ); } /* - * Blue atmosphere crescent on the sun limb + soft edge wash that - * dissolves with the feathered mask. + * Blue atmospheric limb (scattered sunlight) — stays on the sphere only. */ .login-planet__earth::after { content: ""; @@ -6901,23 +6909,23 @@ pre { pointer-events: none; background: radial-gradient( - 130% 100% at 6% 10%, - rgba(90, 160, 255, 0.62) 0%, - rgba(50, 120, 230, 0.36) 24%, - rgba(30, 90, 200, 0.12) 46%, - transparent 64% + 120% 95% at 10% 12%, + rgba(55, 130, 230, 0.42) 0%, + rgba(40, 110, 210, 0.22) 28%, + rgba(30, 90, 185, 0.08) 48%, + transparent 66% ), radial-gradient( circle at 50% 50%, - transparent 72%, - rgba(40, 105, 215, 0.16) 86%, - rgba(55, 130, 235, 0.32) 93%, - rgba(70, 150, 245, 0.12) 98%, + transparent 74%, + rgba(40, 105, 200, 0.14) 88%, + rgba(50, 120, 215, 0.22) 94%, transparent 100% ); box-shadow: - inset 28px 32px 36px 4px rgba(70, 140, 235, 0.16), - inset -22px -26px 48px 12px rgba(0, 6, 20, 0.28); + inset 0 0 20px 4px rgba(45, 110, 200, 0.28), + inset 22px 26px 32px 4px rgba(60, 130, 220, 0.12), + inset -20px -24px 40px 10px rgba(0, 6, 20, 0.3); animation: login-earth-inner 2.2s ease-in-out 0.7s both; } @@ -6938,7 +6946,7 @@ pre { .login-page__rail { position: relative; - z-index: 3; + z-index: 5; grid-column: 2; display: flex; align-items: center; @@ -7215,16 +7223,26 @@ pre { } } +@keyframes login-flare-pulse-a { + from { opacity: 0.7; transform: scale(0.96); } + to { opacity: 1; transform: scale(1.08); } +} + @media (prefers-reduced-motion: reduce) { /* Keep ambient earth spin; only drop entrance flourishes. */ .login-planet, .login-planet__earth::after, .login-card, .login-card__brand, - .login-card__error { + .login-card__error, + .login-page__flare { animation: none !important; } + .login-page__flare { + opacity: 0.85; + } + .login-planet { transform: rotate(5deg); } diff --git a/web/src/pages/LoginShell.tsx b/web/src/pages/LoginShell.tsx index fdd2136..8a0ce00 100644 --- a/web/src/pages/LoginShell.tsx +++ b/web/src/pages/LoginShell.tsx @@ -5,13 +5,16 @@ export function LoginShell({ children }: { children: ReactNode }) { return (
{t("topology.termConnect.hint")}
+