Browse Source

Report DOWN after confirmed port response loss and update builds

main
gudae 1 month ago
parent
commit
8f4d376026
  1. 2
      backend/internal/api/serial_runtime_test.go
  2. 4
      backend/internal/logruntime/scheduler_test.go
  3. 5
      backend/internal/serial/connection_status.go
  4. 28
      backend/internal/serial/connection_status_test.go
  5. 4
      final-build/README.md
  6. 4
      final-build/SHA256SUMS
  7. BIN
      final-build/backend/tera-clone-demo-api
  8. BIN
      final-build/frontend/tera-clone-demo-web
  9. 21
      final-build/release-manifest.json
  10. 4
      frontend/web/static/js/integration.js
  11. 2
      frontend/web/templates/layout.html

2
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"},

4
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,

5
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"
}

28
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)
}
}

4
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 문법 검사를 통과했습니다.

4
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

BIN
final-build/backend/tera-clone-demo-api

Binary file not shown.

BIN
final-build/frontend/tera-clone-demo-web

Binary file not shown.

21
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",

4
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) {

2
frontend/web/templates/layout.html

@ -188,7 +188,7 @@
<script src="/static/js/log-view.js?v=20260914-fixed-port-positions-2"></script>
<script src="/static/js/port-log-clear.js?v=20260914-fixed-port-positions-2"></script>
<script src="/static/js/firmware-progress.js?v=20260908-firmware-progress-1"></script>
<script src="/static/js/integration.js?v=20260916-shared-port-sessions-1"></script>
<script src="/static/js/integration.js?v=20260921-port-down"></script>
</body>
</html>
{{end}}

Loading…
Cancel
Save