diff --git a/backend/internal/api/serial_runtime_test.go b/backend/internal/api/serial_runtime_test.go index 1e536ce..cadcdf6 100644 --- a/backend/internal/api/serial_runtime_test.go +++ b/backend/internal/api/serial_runtime_test.go @@ -284,7 +284,7 @@ func TestRuntimePortStatusRequiresUpBeforeDownAndPrioritizesSession(t *testing.T {name: "boot", runtime: &domain.PortRuntime{}, want: "unknown"}, {name: "unconfirmed miss", runtime: &domain.PortRuntime{CRProbeState: serial.CRProbeStateNoResponse}, want: "unknown"}, {name: "confirmed response", runtime: &domain.PortRuntime{CRProbeEnabled: true, CRProbeUp: true, CRProbeEverUp: true}, want: "up"}, - {name: "no response is not cable down", runtime: &domain.PortRuntime{CRProbeEnabled: true, CRProbeEverUp: true, CRProbeState: serial.CRProbeStateNoResponse}, want: "unknown"}, + {name: "no response after confirmed up", runtime: &domain.PortRuntime{CRProbeEnabled: true, CRProbeEverUp: true, CRProbeState: serial.CRProbeStateNoResponse}, want: "down"}, {name: "no RX timestamp", runtime: &domain.PortRuntime{RXAgeMillis: 0}, want: "unknown"}, {name: "confirmed hardware down", runtime: &domain.PortRuntime{LinkAvailable: true, LinkEverUp: true, LinkState: "down", CRProbeEnabled: true, CRProbeUp: true}, want: "down"}, {name: "unknown session is not promoted", runtime: &domain.PortRuntime{SessionActive: true}, want: "unknown"}, diff --git a/backend/internal/logruntime/scheduler_test.go b/backend/internal/logruntime/scheduler_test.go index 745c499..fd62683 100644 --- a/backend/internal/logruntime/scheduler_test.go +++ b/backend/internal/logruntime/scheduler_test.go @@ -154,8 +154,8 @@ func TestSerialObserverUsesCRProbeForUnifiedConnectionTransitions(t *testing.T) Channel: "CH06", Enabled: true, State: serial.CRProbeStateNoResponse, Up: false, EverUp: true, }}} values, err = observer.Observe(context.Background(), now.Add(time.Second)) - if err != nil || len(values) != 1 || values[0].State != "unknown" || values[0].Signal != "cr" { - t.Fatalf("CR no response must remain indeterminate = %+v, %v", values, err) + if err != nil || len(values) != 1 || values[0].State != "down" || values[0].Signal != "cr" { + t.Fatalf("CR no response after UP must be DOWN = %+v, %v", values, err) } observer.Probes = fakeCRProbeController{values: []serial.CRProbeState{{ Channel: "CH06", Enabled: true, EverUp: true, LastResponseAt: &now, diff --git a/backend/internal/serial/connection_status.go b/backend/internal/serial/connection_status.go index eac5f06..fcf3b92 100644 --- a/backend/internal/serial/connection_status.go +++ b/backend/internal/serial/connection_status.go @@ -3,7 +3,7 @@ package serial import "time" // Allow several normal four-second sweeps to recover from a slow/noisy reply. -// A CR timeout alone cannot establish physical cable disconnection. +// After a confirmed UP, sustained response loss is reported as DOWN. const CRResponseGracePeriod = 15 * time.Second func CRConnectionStatus(probe CRProbeState, now time.Time) string { @@ -19,5 +19,8 @@ func CRConnectionStatus(probe CRProbeState, now time.Time) string { return "up" } } + if probe.EverUp { + return "down" + } return "unknown" } diff --git a/backend/internal/serial/connection_status_test.go b/backend/internal/serial/connection_status_test.go index 9bae46f..27446c8 100644 --- a/backend/internal/serial/connection_status_test.go +++ b/backend/internal/serial/connection_status_test.go @@ -5,7 +5,7 @@ import ( "time" ) -func TestCRConnectionStatusToleratesTransientMissWithoutInventingCableDown(t *testing.T) { +func TestCRConnectionStatusReportsDownAfterConfirmedResponseExpires(t *testing.T) { now := time.Date(2026, 9, 14, 1, 0, 0, 0, time.UTC) probe := CRProbeState{Enabled: true, EverUp: true, LastResponseAt: &now} for _, state := range []string{CRProbeStateNoResponse, CRProbeStateBusy, CRProbeStateSuppressed, CRProbeStateError} { @@ -15,8 +15,8 @@ func TestCRConnectionStatusToleratesTransientMissWithoutInventingCableDown(t *te t.Fatalf("transient %s at %s = %s", state, age, got) } } - if got := CRConnectionStatus(probe, now.Add(CRResponseGracePeriod+time.Millisecond)); got != "unknown" { - t.Fatalf("stale %s = %s; no response cannot prove cable down", state, got) + if got := CRConnectionStatus(probe, now.Add(CRResponseGracePeriod+time.Millisecond)); got != "down" { + t.Fatalf("stale %s = %s; previously confirmed port must become down", state, got) } } probe.EverUp = false @@ -25,10 +25,30 @@ func TestCRConnectionStatusToleratesTransientMissWithoutInventingCableDown(t *te } probe.Up = true if CRConnectionStatus(probe, now) != "up" { - t.Fatal("confirmed reply ignored") + t.Fatal("confirmed reply ignored") } probe.Enabled = false if CRConnectionStatus(probe, now) != "unknown" { t.Fatal("disabled probe promoted") } } + +func TestCRConnectionStatusUnknownUpDownUp(t *testing.T) { + now := time.Now() + probe := CRProbeState{Enabled: true, State: CRProbeStateNoResponse} + if got := CRConnectionStatus(probe, now); got != "unknown" { + t.Fatalf("initial = %s", got) + } + probe.Up, probe.EverUp, probe.LastResponseAt = true, true, &now + if got := CRConnectionStatus(probe, now); got != "up" { + t.Fatalf("response = %s", got) + } + probe.Up = false + if got := CRConnectionStatus(probe, now.Add(CRResponseGracePeriod+time.Second)); got != "down" { + t.Fatalf("response lost = %s", got) + } + probe.Up = true + if got := CRConnectionStatus(probe, now.Add(time.Minute)); got != "up" { + t.Fatalf("response recovered = %s", got) + } +} diff --git a/final-build/README.md b/final-build/README.md index ac8a08d..f7c3fdf 100644 --- a/final-build/README.md +++ b/final-build/README.md @@ -1,6 +1,8 @@ # 최종 장비 빌드 -**릴리스 20260921-snmp-diagnostics-1 · Linux ARM64 · 192.168.0.223 배포 완료** +**릴리스 20260921-port-down-1 · Linux ARM64 · 192.168.0.223 배포 완료** + +2026-09-21 추가 수정: 최초 연결 미확인 상태는 UNKNOWN으로 유지하고, UP 확인 이후 응답이 끊기면 15초 유예 후 DOWN으로 판정합니다. 응답이 복구되면 UP으로 돌아옵니다. API 상태와 링크 이벤트가 같은 판정을 사용하며 웹에서는 DOWN을 빨간색으로 표시합니다. serial/logruntime/API 패키지 테스트와 JavaScript 문법 검사를 통과했습니다. 배포 API·웹 해시 및 integration.js 일치를 확인했습니다. 이번 검증에서 실제 케이블을 분리하지는 않았습니다. 이번 릴리스 커밋에는 DOWN 판정 관련 소스·회귀 테스트를 함께 포함하며, 이전 배포분의 나머지 소스 수정은 작업 트리에 남아 있습니다. 2026-09-21: SNMP v3 사용자 이름의 대소문자를 유지하도록 API를 수정했습니다. PC에서 v3 GET과 SHA-1/AES Trap 수신을 확인했습니다. Trap 시험에는 PC에서 장비 `192.168.0.223`의 UDP 2162 수신을 허용해야 합니다. 진단도구 상단의 초록색 `C:\>`와 `CMD` 표기를 제거하고 본문 프롬프트를 `$`로 변경했습니다. API·웹 배포 해시와 서비스 준비 상태 12/12, 배포된 integration.js/i18n.js/style.css 원문 일치를 확인했습니다. 관련 SNMP 회귀 테스트와 JavaScript 문법 검사를 통과했습니다. diff --git a/final-build/SHA256SUMS b/final-build/SHA256SUMS index 41c153b..1aea215 100644 --- a/final-build/SHA256SUMS +++ b/final-build/SHA256SUMS @@ -1,5 +1,5 @@ -ee1403bd9897df8b94e76058c2b4f198e12d251cf9ff97cb0e346263991469d4 frontend/tera-clone-demo-web -c45e8b57365ac7070c96997ce32a3d8775e7c1ea29ffe1c29ddc886b857caa9c backend/tera-clone-demo-api +96c2f7e0e09ff3357cb62fdfd5b1bc4495c99215d7561a41717b747ee3695a21 frontend/tera-clone-demo-web +c2a947b6573a4c56468c28d156b3ce35b626656c6a9f5b5acdda53ad10da9375 backend/tera-clone-demo-api b5856899698bfafb1f382a3dd45db02b034ba785d56a075ea964c937b57a48b3 backend/tera-logd ac3149f54d93ce252527606bda35dc739f1f3ad40bfb77409f16b3514d437006 backend/tera-clone-demo-alarmd 2864dbde4fcde16898a3b187716b325883365a1fd39f16264a5186877f94c50c backend/tera-netd diff --git a/final-build/backend/tera-clone-demo-api b/final-build/backend/tera-clone-demo-api index 119617d..4443c0e 100644 Binary files a/final-build/backend/tera-clone-demo-api and b/final-build/backend/tera-clone-demo-api differ diff --git a/final-build/frontend/tera-clone-demo-web b/final-build/frontend/tera-clone-demo-web index f0b55ff..0c15a2d 100644 Binary files a/final-build/frontend/tera-clone-demo-web and b/final-build/frontend/tera-clone-demo-web differ diff --git a/final-build/release-manifest.json b/final-build/release-manifest.json index 4727b68..bc858fe 100644 --- a/final-build/release-manifest.json +++ b/final-build/release-manifest.json @@ -1,20 +1,20 @@ { - "release_id": "20260921-snmp-diagnostics-1", + "release_id": "20260921-port-down-1", "platform": "linux-arm64", "scope": "application-and-daemon-binaries-not-full-firmware", - "generated_at": "2026-09-21T13:44:32.1572567+09:00", + "generated_at": "2026-09-21T15:05:03.204065+09:00", "files": [ { "path": "frontend/tera-clone-demo-web", "bytes": 19064981, - "sha256": "ee1403bd9897df8b94e76058c2b4f198e12d251cf9ff97cb0e346263991469d4", + "sha256": "96c2f7e0e09ff3357cb62fdfd5b1bc4495c99215d7561a41717b747ee3695a21", "install_path": "/usr/bin/tera-clone-demo-web", "provenance": "built-and-deployed" }, { "path": "backend/tera-clone-demo-api", - "bytes": 14458265, - "sha256": "c45e8b57365ac7070c96997ce32a3d8775e7c1ea29ffe1c29ddc886b857caa9c", + "bytes": 14458233, + "sha256": "c2a947b6573a4c56468c28d156b3ce35b626656c6a9f5b5acdda53ad10da9375", "install_path": "/usr/bin/tera-api", "provenance": "built-and-deployed" }, @@ -63,7 +63,7 @@ ], "source": { "base_commit": "6ed3580", - "changes": "Built from the working tree based on base_commit, including pending shared-session, tab, log UI, SNMP username case and diagnostics prompt changes. This release commit packages binaries and metadata only; working-tree source changes are not included." + "changes": "Working-tree build based on 6ed3580 with earlier deployed changes and the port DOWN fix. This release includes the DOWN classification source and regression tests; other earlier working-tree source changes remain outside this commit." }, "deployment": { "device": "192.168.0.223", @@ -73,7 +73,7 @@ "total": 12, "backup": "/userdata/tera-backups/telnet-session-ips-20260916-124023", "web_labels_backup": "/userdata/tera-backups/compact-port-selector-20260916-125224", - "verified": "API and web installed SHA256 verified on device; readiness 12/12. Served integration.js, i18n.js and style.css match local source. SNMP v3 GET and Trap reception confirmed.", + "verified": "Installed API and web SHA256 verified, service readiness 12/12, served integration.js matches source.", "web_alias_backup": "/userdata/tera-backups/port-selector-alias-20260914", "web_title_backup": "/userdata/tera-backups/direct-mode-title-20260914", "web_modal_enter_backup": "/userdata/tera-backups/modal-enter-send-20260917-112546", @@ -82,7 +82,9 @@ "web_storage_summary_backup": "/userdata/tera-backups/log-storage-summary-20260917-114241", "web_tab_controls_backup": "/userdata/tera-backups/tab-controls-20260917-115134", "api_snmp_username_backup": "/userdata/tera-backups/snmp-username-20260921-102935", - "web_diagnostics_backup": "/userdata/tera-backups/tab-controls-20260921-134221" + "web_diagnostics_backup": "/userdata/tera-backups/tab-controls-20260921-134221", + "port_down_api_backup": "/userdata/tera-backups/snmp-username-20260921-150246", + "port_down_web_backup": "/userdata/tera-backups/tab-controls-20260921-150326" }, "validation": { "backend": "directstream, portservice, mock store, API, tera-sessiond and tera-api suites passed. Mixed browser/terminal four-session limit, fifth-session rejection, fanout, shared writes, slot reuse, per-session release and combined count covered.", @@ -91,9 +93,10 @@ "browser": "Local Edge: port selector aliases, truncation/hover/accessibility, one-tab navigation, checkboxes and responsive layout; ASCII/HEX display with lowercase values passed.", "live": "ARM64 tests executed on the target: WebSocket plus Unix gateway attachments, same-number operation 1/2 SSH+Telnet, SSH+SSH and Telnet+Telnet over test listeners with a simulated UART all passed. Deployed API, sessiond and web hashes, shared socket and readiness verified.", "limitations": "No PuTTY GUI or simultaneous clients through both physical Ethernet networks acceptance test; integration tests use loopback listeners and a simulated UART. No cable-unplug or power-cycle test.", - "latest": "SNMP mock-store and controller regression tests passed; 17 workspace/tab tests passed; diagnostics JavaScript syntax checks passed. User confirmed SNMP v3 GET and authenticated encrypted Trap reception on PC. Trap reception required a PC firewall exception for device UDP 2162." + "latest": "serial, logruntime and API package tests passed; UNKNOWN -> UP -> DOWN -> UP regression passed with 15-second grace. JavaScript syntax passed. Physical cable unplug not performed for this release." }, "changes": [ + "After confirmed UP, sustained CR response loss becomes DOWN after 15 seconds; never-confirmed ports remain UNKNOWN; applies to API and link events", "Preserve SNMPv3 username case through storage and runtime application", "Remove green C-drive prompt and CMD label from diagnostics; use dollar prompts", "Alt-click inactive tab close retains the selected tab; Alt-click selected tab close retains the leftmost tab", diff --git a/frontend/web/static/js/integration.js b/frontend/web/static/js/integration.js index 2da4e67..b9f7fd5 100644 --- a/frontend/web/static/js/integration.js +++ b/frontend/web/static/js/integration.js @@ -662,12 +662,12 @@ return { active: true, tone: "session", label: "Session_" + sessionCount, sessionCount: sessionCount, summary: "포트 세션 " + sessionCount + "명 연결 중" }; } if (status === "down") { - return { active: false, tone: "error", label: "DOWN", summary: "링크 신호 끊김 확인" }; + return { active: false, tone: "error", label: "DOWN", summary: "연결 확인 후 장비 응답 중단 또는 링크 신호 끊김" }; } if (status === "up") { return { active: true, tone: "connected", label: "UP", summary: "상대 장비 응답 확인 · 일시적 응답 누락은 15초간 재확인" }; } - return { active: false, tone: "waiting", label: "Unknown", summary: "장비 응답 미확인 · 응답이 없다는 이유만으로 케이블 단절을 판정할 수 없습니다." }; + return { active: false, tone: "waiting", label: "Unknown", summary: "아직 장비 연결이 확인되지 않았습니다." }; } function normalizePortView(view) { diff --git a/frontend/web/templates/layout.html b/frontend/web/templates/layout.html index 3dc1a20..35df65d 100644 --- a/frontend/web/templates/layout.html +++ b/frontend/web/templates/layout.html @@ -188,7 +188,7 @@ - + {{end}}