You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1461 lines
61 KiB
1461 lines
61 KiB
package mock
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"teraclone/internal/config"
|
|
)
|
|
|
|
type PageData struct {
|
|
AppName string
|
|
MockMode bool
|
|
CurrentUser string
|
|
CurrentRole string
|
|
WorkspaceID string
|
|
CurrentPath string
|
|
Title string
|
|
Section string
|
|
Breadcrumbs []string
|
|
WorkspaceTabs []WorkspaceTab
|
|
Menu []MenuGroup
|
|
SubTabs []SubTab
|
|
Blocks []ContentBlock
|
|
}
|
|
|
|
type WorkspaceTab struct {
|
|
Key string
|
|
Label string
|
|
Path string
|
|
Active bool
|
|
}
|
|
|
|
type MenuGroup struct {
|
|
Label string
|
|
Icon string
|
|
Path string
|
|
Active bool
|
|
Expanded bool
|
|
Children []MenuChild
|
|
}
|
|
|
|
type MenuChild struct {
|
|
Label string
|
|
Path string
|
|
Active bool
|
|
}
|
|
|
|
type SubTab struct {
|
|
Label string
|
|
Path string
|
|
Active bool
|
|
}
|
|
|
|
type ContentBlock struct {
|
|
Kind string
|
|
Title string
|
|
Subtitle string
|
|
PortLinks []PortLink
|
|
KeyValues []KeyValue
|
|
Form *FormData
|
|
Table *TableData
|
|
Action *ActionPanel
|
|
Tool *ToolData
|
|
Terminal *SerialTerminalData
|
|
Modal *ModalData
|
|
Message string
|
|
}
|
|
|
|
type SerialTerminalData struct {
|
|
Port int
|
|
Options []Option
|
|
}
|
|
|
|
type PortLink struct {
|
|
Number int
|
|
Up bool
|
|
Blink bool
|
|
}
|
|
|
|
type KeyValue struct {
|
|
Key string
|
|
Value string
|
|
}
|
|
|
|
type FormData struct {
|
|
Action string
|
|
PrimaryLabel string
|
|
SecondaryLabel string
|
|
Fields []FormField
|
|
PortTargets []PortTarget
|
|
Notes []string
|
|
}
|
|
|
|
type FormField struct {
|
|
Label string
|
|
Name string
|
|
Type string
|
|
Value string
|
|
Placeholder string
|
|
Required bool
|
|
Disabled bool
|
|
Fixed bool
|
|
Checked bool
|
|
Hint string
|
|
Rows int
|
|
Min string
|
|
Max string
|
|
Accept string
|
|
Options []Option
|
|
Group string
|
|
Hidden bool
|
|
}
|
|
|
|
type Option struct {
|
|
Label string
|
|
Value string
|
|
Selected bool
|
|
}
|
|
|
|
type PortTarget struct {
|
|
Number int
|
|
Checked bool
|
|
}
|
|
|
|
type TableData struct {
|
|
Columns []string
|
|
Rows []TableRow
|
|
EmptyMessage string
|
|
PrimaryLabel string
|
|
SecondaryLabel string
|
|
}
|
|
|
|
type TableRow struct {
|
|
Cells []string
|
|
Actions []RowAction
|
|
}
|
|
|
|
type RowAction struct {
|
|
Label string
|
|
Action string
|
|
Variant string
|
|
Target string
|
|
}
|
|
|
|
type ActionPanel struct {
|
|
Description string
|
|
Buttons []ActionButton
|
|
}
|
|
|
|
type ActionButton struct {
|
|
Label string
|
|
Action string
|
|
Variant string
|
|
}
|
|
|
|
type ToolData struct {
|
|
Action string
|
|
PrimaryLabel string
|
|
Tabs []SubTab
|
|
Fields []FormField
|
|
Result string
|
|
}
|
|
|
|
type ModalData struct {
|
|
Root string
|
|
Title string
|
|
Message string
|
|
Fields []FormField
|
|
Buttons []ActionButton
|
|
}
|
|
|
|
func ResolvePage(path string) (PageData, bool) {
|
|
switch {
|
|
case path == "/":
|
|
return dashboardPage(), true
|
|
case path == "/config/save":
|
|
return actionPage(path, "설정 저장", "보류 중인 설정을 장비 구성 파일에 영구 저장합니다.", "save-config"), true
|
|
case path == "/ports/restart":
|
|
return actionPage(path, "포트 재시작", "포트 설정 화면에서 선택한 채널을 백엔드에서 재시작합니다.", "restart-ports"), true
|
|
case path == "/device/factory-reset":
|
|
return newPage(path, "공장 초기화", "공장 초기화", []string{"공장 초기화"}, nil, []ContentBlock{{
|
|
Kind: "form", Title: "공장 초기화", Subtitle: "저장된 장비 구성을 기본값으로 되돌리는 작업입니다. 실행 전 구성 백업을 권장합니다.",
|
|
Form: &FormData{Action: "factory-reset", PrimaryLabel: "공장 초기화 실행", Fields: []FormField{
|
|
{Label: "확인 문구", Name: "confirmation", Type: "text", Required: true, Placeholder: "공장 초기화", Hint: "실행하려면 ‘공장 초기화’를 정확히 입력하세요."},
|
|
}, Notes: []string{
|
|
"네트워크·포트·외부 인증 등 저장된 구성과 보류 중인 변경이 기본값으로 초기화됩니다.",
|
|
"사용자·사용자 정의 그룹·세션·로그·인증서·저장된 인증정보와 장비 내 진단 파일을 삭제합니다. 기본 계정 terauser / terauser만 복원됩니다.",
|
|
"초기화가 완료되면 자동 재부팅됩니다. 설치된 펌웨어는 유지됩니다.",
|
|
"연결이 끊어집니다. 재부팅 후 기본 접속 주소에서 terauser / terauser로 로그인하세요.",
|
|
}},
|
|
}}), true
|
|
case path == "/device/restart":
|
|
return actionPage(path, "장치 재시작", "현재 장비의 시스템 재시작 작업을 요청합니다.", "restart-device"), true
|
|
case strings.HasPrefix(path, "/network/"):
|
|
return networkPage(path)
|
|
case strings.HasPrefix(path, "/ports/"):
|
|
return portPage(path)
|
|
case strings.HasPrefix(path, "/port-debug"):
|
|
return portDebugPage(path)
|
|
case strings.HasPrefix(path, "/admin/"):
|
|
return adminPage(path)
|
|
case strings.HasPrefix(path, "/settings/"):
|
|
return settingsPage(path)
|
|
case strings.HasPrefix(path, "/tools/"):
|
|
return toolsPage(path)
|
|
case strings.HasPrefix(path, "/logs/"):
|
|
return logsPage(path)
|
|
case path == "/port-status":
|
|
return portStatusPage(), true
|
|
case path == "/password":
|
|
return passwordPage(), true
|
|
default:
|
|
return PageData{}, false
|
|
}
|
|
}
|
|
|
|
func dashboardPage() PageData {
|
|
return newPage("/", "대시보드", "대시보드", []string{"대시보드"}, nil, []ContentBlock{
|
|
{Kind: "port-links", Title: "포트 링크 상태", PortLinks: portLinks()},
|
|
{
|
|
Kind: "key-values",
|
|
Title: "시스템 정보",
|
|
KeyValues: []KeyValue{
|
|
{"모델", "TERA CORA-X48 PRO K"},
|
|
{"빌드번호", "N010-0020-2609-0062"},
|
|
{"시리얼번호", "RMM4027N850000026090062"},
|
|
{"펌웨어", "6.4.6004"},
|
|
{"가동 시간", "2일 17시간 24분 50초"},
|
|
{"시스템 시간", "2026년 6월 24일 수요일 오후 2시 03분 22초"},
|
|
{"CPU 평균", "0.46 / 0.45 / 0.41"},
|
|
},
|
|
},
|
|
{
|
|
Kind: "key-values",
|
|
Title: "네트워크 정보",
|
|
KeyValues: []KeyValue{
|
|
{"인터페이스 1", "eth0 / 192.168.0.233 / 1000Mbps / 링크 연결"},
|
|
{"인터페이스 2", "eth1 / 192.168.200.233 / 1000Mbps / Unknown"},
|
|
{"MAC 주소", "6C:B3:50:0D:36:12"},
|
|
{"다운로드 속도", "0 B/s"},
|
|
{"업로드 속도", "0 B/s"},
|
|
},
|
|
},
|
|
{
|
|
Kind: "key-values",
|
|
Title: "리소스 상태",
|
|
KeyValues: []KeyValue{
|
|
{"CPU 사용률", "14%"},
|
|
{"전체 메모리", "2 GB"},
|
|
{"사용 중 메모리", "1.3 GB"},
|
|
{"남은 메모리", "768 MB"},
|
|
{"메모리 사용률", "62.5%"},
|
|
{"온도", "53.8°C"},
|
|
{"활성 세션", "0"},
|
|
{"활성 알람", "0"},
|
|
},
|
|
},
|
|
{
|
|
Kind: "key-values",
|
|
Title: "포트 사용 현황",
|
|
KeyValues: []KeyValue{
|
|
{"활성 포트", "0"},
|
|
{"누적 수신", "0 B"},
|
|
{"누적 송신", "0 B"},
|
|
{"1위", "CH01 / 0 B"},
|
|
{"2위", "CH02 / 0 B"},
|
|
{"3위", "CH03 / 0 B"},
|
|
{"4위", "CH04 / 0 B"},
|
|
{"5위", "CH05 / 0 B"},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func networkPage(path string) (PageData, bool) {
|
|
subtabs := []SubTab{
|
|
{"인터페이스 1", "/network/interface-1", path == "/network/interface-1"},
|
|
{"인터페이스 2", "/network/interface-2", path == "/network/interface-2"},
|
|
{"호스트 설정", "/network/host", path == "/network/host"},
|
|
{"고급 설정", "/network/advanced", path == "/network/advanced"},
|
|
{"DDNS 설정", "/network/ddns", path == "/network/ddns"},
|
|
}
|
|
|
|
switch path {
|
|
case "/network/interface-1", "/network/interface-2":
|
|
label := "인터페이스 1"
|
|
addr := "192.168.0.233"
|
|
gateway := "192.168.0.1"
|
|
if path == "/network/interface-2" {
|
|
label = "인터페이스 2"
|
|
addr = "192.168.200.233"
|
|
gateway = "192.168.200.1"
|
|
}
|
|
return newPage(path, "네트워크 설정", "네트워크 설정", []string{"네트워크 설정", label}, subtabs, []ContentBlock{
|
|
formBlock(label+" IPv4 설정", "network-apply", []FormField{
|
|
toggleField("DHCP 사용", "dhcp", false),
|
|
textField("IPv4 주소", "address", addr, true),
|
|
textField("서브넷 마스크", "netmask", "255.255.255.0", true),
|
|
textField("게이트웨이", "gateway", gateway, false),
|
|
textField("DNS 서버 1", "dns1", "8.8.8.8", false),
|
|
textField("DNS 서버 2", "dns2", "1.1.1.1", false),
|
|
groupedField(selectField("IPv6 모드", "ipv6mode", "Auto", []string{"Auto", "Disable", "Static"}), "ipv6", true),
|
|
groupedField(textField("IPv6 주소", "ipv6address", "", false), "ipv6", true),
|
|
groupedField(textField("Prefix Length", "prefix", "64", false), "ipv6", true),
|
|
groupedField(textField("IPv6 게이트웨이", "ipv6gateway", "", false), "ipv6", true),
|
|
groupedField(textField("IPv6 DNS 서버 1", "ipv6dns1", "", false), "ipv6", true),
|
|
groupedField(textField("IPv6 DNS 서버 2", "ipv6dns2", "", false), "ipv6", true),
|
|
}, "적용", ""),
|
|
}), true
|
|
case "/network/host":
|
|
return newPage(path, "네트워크 설정", "네트워크 설정", []string{"네트워크 설정", "호스트 설정"}, subtabs, []ContentBlock{
|
|
formBlock("호스트 설정", "network-host", []FormField{
|
|
textField("호스트 이름", "hostname", "teraclone-x48", true),
|
|
}, "적용", ""),
|
|
}), true
|
|
case "/network/advanced":
|
|
return newPage(path, "네트워크 설정", "네트워크 설정", []string{"네트워크 설정", "고급 설정"}, subtabs, []ContentBlock{
|
|
formBlock("운영 모드", "network-advanced", []FormField{
|
|
selectOptionsField("운영 모드", "mode", "single", []Option{{Label: "Single", Value: "single"}, {Label: "OOB", Value: "oob"}}),
|
|
selectOptionsField("고급 네트워크 모드", "advancedMode", "single", []Option{{Label: "Single", Value: "single"}, {Label: "Bonding", Value: "bonding"}, {Label: "Bridge", Value: "bridge"}}),
|
|
groupedField(selectOptionsField("본딩 모드", "bondMode", "active-backup", []Option{{Label: "로드 밸런싱", Value: "balance-rr"}, {Label: "링크 중복", Value: "active-backup"}}), "bond-settings", true),
|
|
groupedField(boundedNumberField("링크 감지 시간(ms)", "bondMii", "50", true, "1", "10000"), "bond-settings", true),
|
|
groupedField(toggleField("브리지 STP 사용", "bridgeSTP", false), "bridge-settings", true),
|
|
groupedField(fixedTextField("OOB 주 인터페이스", "primary", "Interface-1 (eth0)"), "oob-settings", true),
|
|
groupedField(fixedTextField("OOB 예비 인터페이스", "fallback", "Interface-2 (eth1)"), "oob-settings", true),
|
|
groupedField(textField("상태 확인 대상", "healthTarget", "1.1.1.1", true), "oob-settings", true),
|
|
groupedField(textField("확인 주기(초)", "healthInterval", "10", true), "oob-settings", true),
|
|
groupedField(textField("전환 실패 횟수", "failureThreshold", "3", true), "oob-settings", true),
|
|
}, "적용", ""),
|
|
}), true
|
|
case "/network/ddns":
|
|
return newPage(path, "네트워크 설정", "네트워크 설정", []string{"네트워크 설정", "DDNS 설정"}, subtabs, []ContentBlock{
|
|
formBlock("DDNS 설정", "network-ddns", []FormField{
|
|
toggleField("DDNS 사용", "ddns", false),
|
|
selectField("DNS 공급자", "provider", "dnspod", []string{"dnspod", "dyndns", "cloudflare", "noip", "freedns", "dynv6", "freemyip", "cloudxns", "3322", "custom"}),
|
|
toggleField("DDNS IPv4 사용", "ipv4Enabled", true),
|
|
toggleField("DDNS IPv6 사용", "ipv6Enabled", false),
|
|
textField("사용자 이름(ID)", "username", "", false),
|
|
passwordField("비밀번호(Token/Secret)", "password", ""),
|
|
textField("IPv4 도메인", "ipv4Domain", "", false),
|
|
textField("IPv6 도메인", "ipv6Domain", "", false),
|
|
textField("IP 확인 주소 (Check IP URL)", "checkIp", "", false),
|
|
textField("Custom Provider", "customProvider", "", false),
|
|
textField("Custom Provider Path", "customPath", "", false),
|
|
}, "적용", ""),
|
|
}), true
|
|
default:
|
|
return PageData{}, false
|
|
}
|
|
}
|
|
|
|
func portPage(path string) (PageData, bool) {
|
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
if len(parts) != 3 || parts[0] != "ports" {
|
|
return PageData{}, false
|
|
}
|
|
|
|
portID, err := strconv.Atoi(parts[1])
|
|
if err != nil || portID < 1 || portID > 48 {
|
|
return PageData{}, false
|
|
}
|
|
|
|
subPath := parts[2]
|
|
subtabs := []SubTab{
|
|
{"매개변수", fmt.Sprintf("/ports/%d/parameters", portID), subPath == "parameters"},
|
|
{"동작 설정 1", fmt.Sprintf("/ports/%d/operation-1", portID), subPath == "operation-1"},
|
|
{"동작 설정 2", fmt.Sprintf("/ports/%d/operation-2", portID), subPath == "operation-2"},
|
|
{"데이터 로그", fmt.Sprintf("/ports/%d/data-log", portID), subPath == "data-log"},
|
|
{"고급 설정", fmt.Sprintf("/ports/%d/advanced", portID), subPath == "advanced"},
|
|
}
|
|
|
|
switch subPath {
|
|
case "parameters":
|
|
return newPortPage(path, portID, subtabs, []ContentBlock{
|
|
portSelectorBlock(),
|
|
{
|
|
Kind: "form",
|
|
Title: fmt.Sprintf("포트 설정 / Port %d", portID),
|
|
Form: &FormData{
|
|
Action: "#",
|
|
PrimaryLabel: "적용",
|
|
Fields: []FormField{
|
|
textField("별칭 이름", "alias", fmt.Sprintf("Port-%02d", portID), false),
|
|
selectField("시리얼 타입", "serialType", "rs232", []string{"rs232"}),
|
|
selectField("Baudrate", "baudrate", "115200", []string{"110", "300", "600", "1200", "2400", "4800", "9600", "19200", "38400", "57600", "115200", "230400", "460800"}),
|
|
selectField("Databit", "databit", "8", []string{"5", "6", "7", "8"}),
|
|
selectField("Parity", "parity", "none", []string{"none", "odd", "even", "space", "mark"}),
|
|
selectField("Stopbit", "stopbit", "1", []string{"1", "1.5", "2"}),
|
|
selectField("Flow control", "flow", "none", []string{"none", "rtscts", "xonxoff"}),
|
|
selectField("RTS control", "rts", "auto", []string{"auto", "on", "off"}),
|
|
selectField("DTR control", "dtr", "auto", []string{"auto", "on", "off"}),
|
|
toggleField("Ignore jammed", "ignore", false),
|
|
},
|
|
PortTargets: portTargets(portID),
|
|
},
|
|
},
|
|
}), true
|
|
case "operation-1":
|
|
return newPortPage(path, portID, subtabs, []ContentBlock{
|
|
portSelectorBlock(),
|
|
formBlock("동작 설정 1", "port-operation-1", []FormField{
|
|
portModeField("reverse_ssh"),
|
|
textField("Listen Port", "port", fmt.Sprintf("100%02d", portID), false),
|
|
fixedTextField("Keep alive(초)", "keepalive", "3"),
|
|
fixedTextField("ACK 타임아웃(ms)", "timeout", "10000"),
|
|
fixedTextField("접속(유휴) 타임아웃(ms)", "connectionTimeout", "0"),
|
|
boundedNumberField("최대 세션 수", "maxClients", "4", true, "1", "4"),
|
|
authenticationField(),
|
|
selectField("줄바꿈", "newline", "cr", []string{"cr", "lf", "crlf"}),
|
|
shortcutField("제어 단축키", "controlShortcut", "^]", "Ctrl+] 후 T/Q/B/I/? 명령을 사용합니다."),
|
|
shortcutField("전환 단축키", "switchShortcut", "^T", "Reverse 모드의 현재 세션과 동시 접속 유지 상태를 확인합니다."),
|
|
shortcutField("세션 종료 단축키", "quitShortcut", "^Q", "현재 터미널 세션만 종료합니다."),
|
|
shortcutField("Break 단축키(선택)", "breakShortcut", "", "비워두면 직접 단축키는 사용하지 않습니다. 예: ^B"),
|
|
shortcutField("인터럽트 단축키(선택)", "interruptShortcut", "", "비워두면 사용하지 않습니다. 예: ^X"),
|
|
}, "적용", ""),
|
|
}), true
|
|
case "operation-2":
|
|
return newPortPage(path, portID, subtabs, []ContentBlock{
|
|
portSelectorBlock(),
|
|
formBlock("동작 설정 2", "port-operation-2", []FormField{
|
|
portModeField("disabled"),
|
|
textField("Listen Port", "port", fmt.Sprintf("110%02d", portID), false),
|
|
fixedTextField("Keep alive(초)", "keepalive", "3"),
|
|
fixedTextField("ACK 타임아웃(ms)", "timeout", "10000"),
|
|
fixedTextField("접속(유휴) 타임아웃(ms)", "connectionTimeout", "0"),
|
|
boundedNumberField("최대 세션 수", "maxClients", "4", true, "1", "4"),
|
|
authenticationField(),
|
|
selectField("줄바꿈", "newline", "cr", []string{"cr", "lf", "crlf"}),
|
|
shortcutField("제어 단축키", "controlShortcut", "^]", "Ctrl+] 후 T/Q/B/I/? 명령을 사용합니다."),
|
|
shortcutField("전환 단축키", "switchShortcut", "^T", "Reverse 모드의 현재 세션과 동시 접속 유지 상태를 확인합니다."),
|
|
shortcutField("세션 종료 단축키", "quitShortcut", "^Q", "현재 터미널 세션만 종료합니다."),
|
|
shortcutField("Break 단축키(선택)", "breakShortcut", "", "비워두면 직접 단축키는 사용하지 않습니다. 예: ^B"),
|
|
shortcutField("인터럽트 단축키(선택)", "interruptShortcut", "", "비워두면 사용하지 않습니다. 예: ^X"),
|
|
}, "적용", ""),
|
|
}), true
|
|
case "data-log":
|
|
return newPortPage(path, portID, subtabs, []ContentBlock{
|
|
portSelectorBlock(),
|
|
formBlock("데이터 로그", "port-data-log", []FormField{
|
|
toggleField("Read 기록", "read", true),
|
|
toggleField("Send 기록", "send", true),
|
|
toggleField("타임스탬프 표시", "showTimestamp", true),
|
|
toggleField("방향 표시", "showDirection", true),
|
|
toggleField("사용자 표시", "showUsername", false),
|
|
toggleField("IP 주소 표시", "showIP", false),
|
|
toggleField("포트 표시", "showPort", false),
|
|
selectField("로그 형식", "format", "ascii", []string{"ascii", "hex"}),
|
|
}, "적용", ""),
|
|
}), true
|
|
case "advanced":
|
|
return newPortPage(path, portID, subtabs, []ContentBlock{
|
|
portSelectorBlock(),
|
|
formBlock("고급 설정", "port-advanced", []FormField{
|
|
FormField{Label: "Alarm 키워드", Name: "alarmKeyword", Type: "text", Hint: "대소문자를 구분하지 않는 문자열 일치 방식입니다. 수신 키워드 감지는 알림 설정에서 켤 수 있습니다."},
|
|
}, "적용", ""),
|
|
}), true
|
|
default:
|
|
return PageData{}, false
|
|
}
|
|
}
|
|
|
|
func portSelectorBlock() ContentBlock {
|
|
return ContentBlock{
|
|
Kind: "port-selector",
|
|
Title: "포트 선택",
|
|
PortLinks: portLinks(),
|
|
}
|
|
}
|
|
|
|
func portDebugPage(path string) (PageData, bool) {
|
|
portID := 1
|
|
if path != "/port-debug" {
|
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
if len(parts) != 2 || parts[0] != "port-debug" {
|
|
return PageData{}, false
|
|
}
|
|
parsed, err := strconv.Atoi(parts[1])
|
|
if err != nil || parsed < 1 || parsed > 48 {
|
|
return PageData{}, false
|
|
}
|
|
portID = parsed
|
|
}
|
|
|
|
options := make([]Option, 0, 48)
|
|
for number := 1; number <= 48; number++ {
|
|
options = append(options, Option{
|
|
Label: fmt.Sprintf("CH%02d / Port %d", number, number),
|
|
Value: strconv.Itoa(number),
|
|
Selected: number == portID,
|
|
})
|
|
}
|
|
|
|
return newPage(path, "포트 디버그", fmt.Sprintf("포트 디버그 / Port %d", portID), []string{"포트 디버그"}, nil, []ContentBlock{
|
|
{
|
|
Kind: "serial-terminal",
|
|
Title: "시리얼 포트 CMD 콘솔",
|
|
Terminal: &SerialTerminalData{
|
|
Port: portID,
|
|
Options: options,
|
|
},
|
|
},
|
|
}), true
|
|
}
|
|
|
|
func adminPage(path string) (PageData, bool) {
|
|
switch path {
|
|
case "/admin/users":
|
|
return newPage(path, "관리자 운영", "사용자 계정", []string{"관리자 운영", "사용자 계정"}, nil, []ContentBlock{
|
|
tableBlock("사용자 계정", []string{"상태", "사용자 이름", "역할", "그룹", "비밀번호", "포트 권한"}, nil, "사용자 추가", ""),
|
|
userAccountModalBlock(),
|
|
}), true
|
|
case "/admin/auth/radius":
|
|
return newPage(path, "관리자 운영", "인증 서버", []string{"관리자 운영", "인증 서버", "RADIUS"}, authServerTabs(path), []ContentBlock{
|
|
formBlock("RADIUS 인증", "external-auth-radius", []FormField{
|
|
toggleField("RADIUS 사용", "enabled", false),
|
|
textField("서버 주소", "host", "", true),
|
|
boundedNumberField("인증 포트", "port", "1645", true, "1", "65535"),
|
|
passwordField("공유 Secret", "secret", "기존 값을 유지하려면 비워두세요"),
|
|
boundedNumberField("응답 타임아웃(초)", "timeout", "5", true, "1", "30"),
|
|
selectOptionsField("외부 사용자 기본 역할", "defaultRole", "port_operator", roleOptions()),
|
|
fixedTextField("장애 시 인증", "fallback", "Local 인증으로 안전하게 전환"),
|
|
}, "적용", ""),
|
|
}), true
|
|
case "/admin/auth/tacacs":
|
|
return newPage(path, "관리자 운영", "인증 서버", []string{"관리자 운영", "인증 서버", "TACACS+"}, authServerTabs(path), []ContentBlock{
|
|
formBlock("TACACS+ 인증", "external-auth-tacacs", []FormField{
|
|
toggleField("TACACS+ 사용", "enabled", false),
|
|
textField("서버 주소", "host", "", true),
|
|
boundedNumberField("인증 포트", "port", "49", true, "1", "65535"),
|
|
passwordField("공유 Secret", "secret", "기존 값을 유지하려면 비워두세요"),
|
|
boundedNumberField("응답 타임아웃(초)", "timeout", "5", true, "1", "30"),
|
|
selectOptionsField("외부 사용자 기본 역할", "defaultRole", "port_operator", roleOptions()),
|
|
fixedTextField("장애 시 인증", "fallback", "Local 인증으로 안전하게 전환"),
|
|
}, "적용", ""),
|
|
}), true
|
|
case "/admin/address-filter":
|
|
return newPage(path, "관리자 운영", "주소 필터", []string{"관리자 운영", "주소 필터"}, addressFilterTabs(path), []ContentBlock{
|
|
formBlock("관리 접속 주소 필터", "address-filter-policy", []FormField{
|
|
toggleField("주소 필터 사용", "enabled", false),
|
|
selectOptionsField("필터 방식", "mode", "blacklist", []Option{{Label: "차단 목록", Value: "blacklist", Selected: true}, {Label: "허용 목록", Value: "whitelist"}}),
|
|
fixedTextField("안전 안내", "safety", "허용 목록 적용 전 현재 PC 주소를 반드시 등록하세요"),
|
|
}, "적용", ""),
|
|
}), true
|
|
case "/admin/address-filter/ipv4", "/admin/address-filter/mac":
|
|
return addressFilterListPage(path), true
|
|
case "/admin/address-filter/ipv4/new", "/admin/address-filter/mac/new":
|
|
return addressFilterNewPage(path), true
|
|
case "/admin/snmp":
|
|
return newPage(path, "관리자 운영", "SNMP", []string{"관리자 운영", "SNMP"}, nil, []ContentBlock{
|
|
formBlock("SNMP Agent", "snmp-agent", []FormField{
|
|
toggleField("SNMP 사용", "enabled", false),
|
|
textField("장비 위치(sysLocation)", "location", "", false),
|
|
textField("관리자 연락처(sysContact)", "contact", "", false),
|
|
toggleField("SNMP v1", "v1", false),
|
|
toggleField("SNMP v2c", "v2c", true),
|
|
toggleField("SNMP v3", "v3", false),
|
|
passwordField("Read Community", "roCommunity", "기존 값을 유지하려면 비워두세요"),
|
|
passwordField("Write Community", "rwCommunity", "기존 값을 유지하려면 비워두세요"),
|
|
textField("v3 사용자", "v3Username", "", false),
|
|
selectOptionsField("v3 접근 권한", "v3Access", "read", []Option{{Label: "읽기", Value: "read", Selected: true}, {Label: "읽기/쓰기", Value: "write"}}),
|
|
selectOptionsField("v3 인증", "v3Auth", "sha256", []Option{{Label: "없음", Value: "none"}, {Label: "MD5", Value: "md5"}, {Label: "SHA-1", Value: "sha1"}, {Label: "SHA-256", Value: "sha256", Selected: true}}),
|
|
selectOptionsField("v3 암호화", "v3Privacy", "aes", []Option{{Label: "없음", Value: "none"}, {Label: "DES", Value: "des"}, {Label: "AES", Value: "aes", Selected: true}}),
|
|
passwordField("v3 인증 키", "v3AuthKey", "기존 값을 유지하려면 비워두세요"),
|
|
passwordField("v3 암호화 키", "v3PrivacyKey", "기존 값을 유지하려면 비워두세요"),
|
|
toggleField("v3 사용자 사용", "v3UserEnabled", false),
|
|
}, "적용", ""),
|
|
}), true
|
|
case "/admin/ssl":
|
|
return newPage(path, "관리자 운영", "SSL 인증서", []string{"관리자 운영", "SSL 인증서"}, sslTabs(path), []ContentBlock{
|
|
formBlock("사용 인증서", "tls-active", []FormField{
|
|
selectOptionsField("활성 인증서", "activeCertificate", "", []Option{{Label: "인증서를 불러오는 중", Value: "", Selected: true}}),
|
|
}, "적용", ""),
|
|
tableBlock("인증서 목록", []string{"ID", "공통 이름", "유효 기간", "키", "상태", "관리"}, nil, "", ""),
|
|
}), true
|
|
case "/admin/ssl/new":
|
|
return newPage(path, "관리자 운영", "SSL 인증서 생성", []string{"관리자 운영", "SSL 인증서", "새로 만들기"}, sslTabs(path), []ContentBlock{
|
|
formBlock("자체 서명 인증서 생성", "tls-create", []FormField{
|
|
fixedTextField("인증서 ID", "certificateID", "접속 주소로 자동 생성"),
|
|
textField("공통 이름(CN)", "commonName", "", true),
|
|
textField("국가(C)", "country", "KR", false),
|
|
textField("시/도(ST)", "province", "", false),
|
|
textField("도시(L)", "locality", "", false),
|
|
textField("조직(O)", "organization", "", false),
|
|
textField("부서(OU)", "organizationalUnit", "", false),
|
|
textField("이메일", "email", "", false),
|
|
}, "생성", ""),
|
|
}), true
|
|
case "/admin/ssl/import":
|
|
return newPage(path, "관리자 운영", "SSL 인증서 가져오기", []string{"관리자 운영", "SSL 인증서", "가져오기"}, sslTabs(path), []ContentBlock{
|
|
formBlock("PEM 인증서 가져오기", "tls-import", []FormField{
|
|
fixedTextField("인증서 ID", "certificateID", "파일명으로 자동 생성"),
|
|
fileField("인증서 파일", "certificateFile", ".pem,.crt,.cer", true),
|
|
fileField("개인 키 파일", "privateKeyFile", ".pem,.key", false),
|
|
passwordField("개인 키 비밀번호", "password", "암호화된 키인 경우 입력"),
|
|
}, "가져오기", ""),
|
|
}), true
|
|
case "/admin/ssl/import-csr":
|
|
return newPage(path, "관리자 운영", "CSR 내보내기", []string{"관리자 운영", "SSL 인증서", "CSR 내보내기"}, sslTabs(path), []ContentBlock{
|
|
formBlock("인증서 서명 요청(CSR)", "tls-csr", []FormField{
|
|
selectOptionsField("인증서", "certificateID", "", []Option{{Label: "인증서를 불러오는 중", Value: "", Selected: true}}),
|
|
textField("공통 이름(CN)", "commonName", "", false),
|
|
textField("조직(O)", "organization", "", false),
|
|
textField("이메일", "email", "", false),
|
|
}, "CSR 다운로드", ""),
|
|
}), true
|
|
case "/admin/backup/export", "/admin/backup/import", "/admin/backup/restore":
|
|
if path == "/admin/backup/export" {
|
|
return newPage(path, "관리자 운영", "구성 내보내기", []string{"관리자 운영", "백업/복원", "내보내기"}, backupTabs(path), []ContentBlock{
|
|
formBlock("암호화 구성 백업", "config-export-encrypted", []FormField{
|
|
passwordField("백업 파일 암호", "key", "8~64자"),
|
|
passwordField("암호 확인", "confirmKey", "백업 파일 암호 재입력"),
|
|
fixedTextField("백업 범위", "scope", "네트워크를 포함한 영구 설정 전체"),
|
|
}, "백업 다운로드", ""),
|
|
}), true
|
|
}
|
|
return newPage(path, "관리자 운영", "구성 복원", []string{"관리자 운영", "백업/복원", "복원"}, backupTabs(path), []ContentBlock{
|
|
formBlock("암호화 백업 복원", "config-restore-encrypted", []FormField{
|
|
fileField("백업 파일", "backupFile", ".json,.tera-config.json", true),
|
|
passwordField("백업 파일 암호", "key", "내보낼 때 사용한 암호"),
|
|
toggleField("네트워크 설정 포함", "includeNetwork", false),
|
|
fixedTextField("안전 안내", "networkWarning", "네트워크 포함 시 현재 웹 연결이 끊길 수 있습니다"),
|
|
}, "복원 및 적용", ""),
|
|
}), true
|
|
case "/admin/firmware":
|
|
block := formBlock("펌웨어 업그레이드", "firmware-upgrade", []FormField{
|
|
fileField("서명된 펌웨어 패키지", "firmwareFile", ".pkg,application/octet-stream", true),
|
|
}, "업데이트 적용", "")
|
|
return newPage(path, "관리자 운영", "펌웨어 업그레이드", []string{"관리자 운영", "펌웨어"}, nil, []ContentBlock{block}), true
|
|
default:
|
|
return PageData{}, false
|
|
}
|
|
}
|
|
|
|
func settingsPage(path string) (PageData, bool) {
|
|
switch path {
|
|
case "/settings/alert/event":
|
|
return newPage(path, "기타 설정", "알림 설정", []string{"기타 설정", "알림 설정", "시스템 이벤트"}, alertTabs(path), []ContentBlock{
|
|
formBlock("시스템 이벤트 알림", "alarm-system-events", []FormField{
|
|
toggleField("Cold start", "coldStart", true),
|
|
toggleField("Warm start", "warmStart", true),
|
|
toggleField("로그인 실패 임계값", "authFailures", true),
|
|
toggleField("OOB 경로 전환", "linkFailover", true),
|
|
toggleField("시리얼 링크 DOWN", "serialLinkDown", true),
|
|
toggleField("이더넷 링크 DOWN", "ethernetLinkDown", true),
|
|
toggleField("저장 공간 경고", "storagePressure", true),
|
|
toggleField("메일로 전달", "deliveryEmail", false),
|
|
toggleField("SNMP Trap으로 전달", "deliverySNMP", false),
|
|
toggleField("Syslog로 전달", "deliverySyslog", true),
|
|
}, "적용", ""),
|
|
}), true
|
|
case "/settings/alert/serial-event":
|
|
return newPage(path, "기타 설정", "알림 설정", []string{"기타 설정", "알림 설정", "시리얼 이벤트"}, alertTabs(path), []ContentBlock{
|
|
formBlock("포트별 시리얼 이벤트", "alarm-serial-events", []FormField{
|
|
selectOptionsField("대상 포트", "channel", "CH01", channelSelectOptions()),
|
|
toggleField("DCD 상태 변경", "dcd", false),
|
|
toggleField("DSR 상태 변경", "dsr", false),
|
|
toggleField("수신 키워드 감지", "keyword", false),
|
|
fixedTextField("감지 키워드", "keywordValue", "포트 설정 > 고급 설정의 Alarm 키워드 사용"),
|
|
toggleField("메일로 전달", "deliveryEmail", false),
|
|
toggleField("SNMP Trap으로 전달", "deliverySNMP", false),
|
|
toggleField("Syslog로 전달", "deliverySyslog", true),
|
|
}, "적용", ""),
|
|
}), true
|
|
case "/settings/alert/mail":
|
|
return newPage(path, "기타 설정", "알림 설정", []string{"기타 설정", "알림 설정", "메일"}, alertTabs(path), []ContentBlock{
|
|
formBlock("메일 알림", "alarm-email", []FormField{
|
|
toggleField("메일 알림 사용", "enabled", false),
|
|
textField("SMTP 서버", "smtp", "", true),
|
|
boundedNumberField("SMTP 포트", "port", "587", true, "1", "65535"),
|
|
toggleField("STARTTLS 사용", "starttls", true),
|
|
toggleField("SMTP 인증 사용", "auth", false),
|
|
textField("SMTP 사용자", "username", "", false),
|
|
passwordField("SMTP 비밀번호", "password", "기존 값을 유지하려면 비워두세요"),
|
|
textField("발신자", "from", "", true),
|
|
textField("수신자", "to", "", true),
|
|
textField("참조(CC)", "cc", "", false),
|
|
textField("제목", "title", "TERA 장비 알림", false),
|
|
textAreaField("본문", "content", "", 4),
|
|
}, "적용", "테스트 발송"),
|
|
}), true
|
|
case "/settings/alert/snmp-trap-filter":
|
|
return newPage(path, "기타 설정", "알림 설정", []string{"기타 설정", "알림 설정", "SNMP Trap"}, alertTabs(path), []ContentBlock{
|
|
formBlock("SNMP Trap 대상", "alarm-snmp-trap", []FormField{
|
|
toggleField("Trap 전송 사용", "enabled", false),
|
|
fixedTextField("대상 ID", "targetID", "primary-trap"),
|
|
textField("대상 주소", "host", "", true),
|
|
boundedNumberField("대상 포트", "port", "162", true, "1", "65535"),
|
|
selectOptionsField("버전", "version", "v2c", []Option{{Label: "v1", Value: "v1"}, {Label: "v2c", Value: "v2c", Selected: true}, {Label: "v3", Value: "v3"}}),
|
|
textField("v3 사용자", "username", "", false),
|
|
selectOptionsField("v3 인증", "authProtocol", "none", []Option{{Label: "없음", Value: "none", Selected: true}, {Label: "MD5", Value: "md5"}, {Label: "SHA-1", Value: "sha1"}, {Label: "SHA-256", Value: "sha256"}}),
|
|
selectOptionsField("v3 암호화", "privacyProtocol", "none", []Option{{Label: "없음", Value: "none", Selected: true}, {Label: "DES", Value: "des"}, {Label: "AES", Value: "aes"}}),
|
|
textField("v3 Engine ID", "engineID", "", false),
|
|
passwordField("Community / 인증 키", "credential", "기존 값을 유지하려면 비워두세요"),
|
|
passwordField("v3 암호화 키", "privacyKey", "기존 값을 유지하려면 비워두세요"),
|
|
}, "적용", "테스트 전송"),
|
|
}), true
|
|
case "/settings/system-log/remote":
|
|
tabs := systemLogSettingsTabs(path)
|
|
block := formBlock("로그 서버", "apply-log-server", []FormField{
|
|
toggleField("원격 로그 전송 사용", "remote", false),
|
|
{Label: "로그 서버", Name: "server", Type: "text", Placeholder: "도메인 또는 IP[:포트] 예: 192.168.0.233:514", Hint: "포트를 생략하면 표준 Syslog 포트 514를 사용합니다."},
|
|
selectField("전송 프로토콜", "protocol", "UDP", []string{"UDP", "TCP"}),
|
|
}, "적용", "")
|
|
block.Subtitle = "Syslog 서버를 도메인 또는 IP 주소로 지정합니다. 포트는 주소 뒤에 콜론(:)으로 입력할 수 있습니다."
|
|
return newPage(path, "기타 설정", "시스템 로그 - 로그 서버", []string{"기타 설정", "시스템 로그", "로그 서버"}, tabs, []ContentBlock{block}), true
|
|
case "/settings/system-log/status-event", "/settings/system-log/events":
|
|
tabs := systemLogSettingsTabs(path)
|
|
block := formBlock("로그 이벤트", "apply-log-events", []FormField{
|
|
toggleField("시스템 이벤트 - 로컬 저장", "localSystem", true),
|
|
toggleField("시스템 이벤트 - 원격 전송", "remoteSystem", false),
|
|
toggleField("설정 변경 이벤트 - 로컬 저장", "localConfiguration", true),
|
|
toggleField("설정 변경 이벤트 - 원격 전송", "remoteConfiguration", false),
|
|
toggleField("운영 모드 이벤트 - 로컬 저장", "localOperation", true),
|
|
toggleField("운영 모드 이벤트 - 원격 전송", "remoteOperation", false),
|
|
}, "적용", "")
|
|
block.Subtitle = "시스템 시작/재시작/로그인 실패, 설정 변경, 포트 운영 세션 이벤트의 저장 위치를 선택합니다."
|
|
block.Form.Notes = []string{
|
|
"시스템 이벤트: Cold start, Warm start, 로그인 실패",
|
|
"설정 변경 이벤트: 사용자가 웹 또는 콘솔에서 장비 설정을 변경한 기록",
|
|
"운영 모드 이벤트: Reverse SSH/Reverse Telnet 세션 연결 및 해제 기록",
|
|
}
|
|
return newPage(path, "기타 설정", "시스템 로그 - 로그 이벤트", []string{"기타 설정", "시스템 로그", "로그 이벤트"}, tabs, []ContentBlock{block}), true
|
|
case "/settings/datetime":
|
|
return simpleFormPage(path, "기타 설정", []string{"기타 설정", "날짜와 시간"}, "날짜와 시간", []FormField{
|
|
selectField("시간대", "timezone", "Asia/Seoul", []string{"Asia/Seoul", "UTC", "America/Los_Angeles"}),
|
|
toggleField("NTP 사용", "ntp", true),
|
|
textField("NTP 서버 1", "ntp1", "time.google.com", false),
|
|
textField("NTP 서버 2", "ntp2", "pool.ntp.org", false),
|
|
textField("수동 날짜", "date", "", false),
|
|
textField("수동 시간", "time", "", false),
|
|
}), true
|
|
default:
|
|
return PageData{}, false
|
|
}
|
|
}
|
|
|
|
func toolsPage(path string) (PageData, bool) {
|
|
switch path {
|
|
case "/tools/nettest/ping":
|
|
fields := []FormField{
|
|
textField("IP 주소 / 도메인 이름", "address", "192.168.0.100", true),
|
|
numberField("핑 횟수", "count", "4", true),
|
|
fixedTextField("핑 패킷 크기", "size", "56 bytes"),
|
|
fixedTextField("핑 응답 타임아웃", "replyTimeout", "1초 (고정)"),
|
|
}
|
|
return newPage(path, "진단 도구", "핑 테스트", []string{"진단 도구", "핑 테스트"}, nil, []ContentBlock{
|
|
{
|
|
Kind: "tool",
|
|
Title: "핑 테스트",
|
|
Tool: &ToolData{
|
|
Action: "#",
|
|
PrimaryLabel: "테스트",
|
|
Fields: fields,
|
|
Result: "테스트를 실행하면 백엔드 진단 작업 결과가 여기에 표시됩니다.",
|
|
},
|
|
},
|
|
}), true
|
|
case "/tools/alias":
|
|
portOptions := make([]Option, 0, 48)
|
|
for portNumber := 1; portNumber <= 48; portNumber++ {
|
|
portOptions = append(portOptions, Option{
|
|
Label: fmt.Sprintf("Port %d (CH%02d)", portNumber, portNumber),
|
|
Value: fmt.Sprintf("CH%02d", portNumber),
|
|
})
|
|
}
|
|
return simpleFormPage(path, "진단 도구", []string{"진단 도구", "포트 별칭"}, "포트 별칭", []FormField{
|
|
selectOptionsField("포트 선택", "channel", "CH01", portOptions),
|
|
textField("포트 별칭", "alias", "", true),
|
|
}), true
|
|
default:
|
|
return PageData{}, false
|
|
}
|
|
}
|
|
|
|
func logsPage(path string) (PageData, bool) {
|
|
tabs := []SubTab{
|
|
{Label: "시스템 로그", Path: "/logs/system", Active: path == "/logs/system"},
|
|
{Label: "포트 로그", Path: "/logs/ports", Active: path == "/logs/ports"},
|
|
{Label: "포트 상태 이벤트", Path: "/logs/links", Active: path == "/logs/links"},
|
|
}
|
|
limitOptions := []Option{
|
|
{Label: "50건", Value: "50"},
|
|
{Label: "100건", Value: "100"},
|
|
{Label: "200건", Value: "200"},
|
|
{Label: "500건", Value: "500"},
|
|
}
|
|
switch path {
|
|
case "/logs/system":
|
|
return newPage(path, "로그", "시스템 로그", []string{"로그", "시스템 로그"}, tabs, []ContentBlock{
|
|
{
|
|
Kind: "form",
|
|
Title: "검색 조건",
|
|
Subtitle: "일반 시스템 이벤트와 보존 대상 감사 기록을 조회합니다. 클리어는 감사 기록을 삭제하지 않습니다.",
|
|
Form: &FormData{
|
|
Action: "#",
|
|
PrimaryLabel: "조회",
|
|
SecondaryLabel: "검색 초기화",
|
|
Fields: []FormField{
|
|
{Label: "로그 종류", Name: "category", Type: "text", Hidden: true},
|
|
selectOptionsField("등급", "severity", "", []Option{{Label: "전체", Value: ""}, {Label: "정보", Value: "info"}, {Label: "경고", Value: "warning"}, {Label: "치명적", Value: "critical"}}),
|
|
{Label: "검색어", Name: "keyword", Type: "text", Placeholder: "소스 또는 메시지"},
|
|
{Label: "시작 시간", Name: "from", Type: "datetime-local"},
|
|
{Label: "종료 시간", Name: "to", Type: "datetime-local"},
|
|
selectOptionsField("페이지당 표시", "limit", "100", limitOptions),
|
|
selectField("다운로드 형식", "export_format", "CSV", []string{"CSV", "JSON", "JSONL"}),
|
|
selectField("최대 다운로드 건수", "export_limit", "1000", []string{"100", "500", "1000", "5000"}),
|
|
toggleField("자동 갱신 (5초)", "auto_refresh", true),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Kind: "table",
|
|
Title: "시스템 로그",
|
|
Subtitle: "최신 이벤트가 위에 표시됩니다.",
|
|
Table: &TableData{
|
|
Columns: []string{"시간", "등급", "분류", "발생 위치", "내용"},
|
|
EmptyMessage: "조건에 맞는 시스템 로그가 없습니다.",
|
|
PrimaryLabel: "새로고침",
|
|
SecondaryLabel: "로그 다운로드",
|
|
},
|
|
},
|
|
}), true
|
|
case "/logs/ports":
|
|
channelOptions := []Option{{Label: "전체 포트", Value: ""}}
|
|
for port := 1; port <= 48; port++ {
|
|
channelOptions = append(channelOptions, Option{Label: fmt.Sprintf("CH%02d", port), Value: fmt.Sprintf("CH%02d", port)})
|
|
}
|
|
return newPage(path, "로그", "포트 로그", []string{"로그", "포트 로그"}, tabs, []ContentBlock{
|
|
{
|
|
Kind: "form",
|
|
Title: "검색 조건",
|
|
Subtitle: "포트별 RX/TX 데이터·명령·접속/종료 기록을 조회합니다. UP/DOWN 변경은 포트 상태 이벤트에서 확인하세요.",
|
|
Form: &FormData{
|
|
Action: "#",
|
|
PrimaryLabel: "조회",
|
|
SecondaryLabel: "검색 초기화",
|
|
Fields: []FormField{
|
|
selectOptionsField("포트", "channel", "", channelOptions),
|
|
{Label: "로그 종류", Name: "event_type", Type: "text", Hidden: true},
|
|
selectOptionsField("방향", "direction", "", []Option{{Label: "전체", Value: ""}, {Label: "RX", Value: "RX"}, {Label: "TX", Value: "TX"}}),
|
|
{Label: "사용자", Name: "actor", Type: "text", Placeholder: "사용자 이름"},
|
|
{Label: "검색어", Name: "keyword", Type: "text", Placeholder: "명령, 결과 또는 메시지"},
|
|
{Label: "시작 시간", Name: "from", Type: "datetime-local"},
|
|
{Label: "종료 시간", Name: "to", Type: "datetime-local"},
|
|
selectOptionsField("페이지당 표시", "limit", "100", limitOptions),
|
|
selectField("다운로드 형식", "export_format", "CSV", []string{"CSV", "JSON", "JSONL"}),
|
|
selectField("최대 다운로드 건수", "export_limit", "1000", []string{"100", "500", "1000", "5000"}),
|
|
toggleField("자동 갱신 (5초)", "auto_refresh", true),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Kind: "table",
|
|
Title: "포트 로그",
|
|
Subtitle: "포트별 통신 이력 · 내보내기는 현재 검색 조건과 최대 다운로드 건수 범위에 적용됩니다.",
|
|
Table: &TableData{
|
|
Columns: []string{"시간", "포트", "유형", "방향", "사용자", "내용"},
|
|
EmptyMessage: "조건에 맞는 포트 로그가 없습니다.",
|
|
PrimaryLabel: "새로고침",
|
|
SecondaryLabel: "로그 다운로드",
|
|
},
|
|
},
|
|
}), true
|
|
case "/logs/links":
|
|
resourceOptions := []Option{{Label: "전체 포트", Value: ""}}
|
|
for port := 1; port <= 48; port++ {
|
|
resourceOptions = append(resourceOptions, Option{Label: fmt.Sprintf("CH%02d", port), Value: fmt.Sprintf("CH%02d", port)})
|
|
}
|
|
return newPage(path, "로그", "포트 상태 이벤트", []string{"로그", "포트 상태 이벤트"}, tabs, []ContentBlock{
|
|
{
|
|
Kind: "form",
|
|
Title: "검색 조건",
|
|
Subtitle: "포트의 UP/DOWN 및 제공되는 신호 변경 기록을 조회합니다. RX/TX 데이터·명령·접속 기록은 포트 로그에서 확인하세요.",
|
|
Form: &FormData{
|
|
Action: "#",
|
|
PrimaryLabel: "조회",
|
|
SecondaryLabel: "검색 초기화",
|
|
Fields: []FormField{
|
|
selectOptionsField("포트", "resource", "", resourceOptions),
|
|
{Label: "이벤트 종류", Name: "event_type", Type: "text", Value: "transition", Hidden: true},
|
|
selectOptionsField("상태", "state", "", []Option{{Label: "전체", Value: ""}, {Label: "UP", Value: "up"}, {Label: "DOWN", Value: "down"}, {Label: "Unknown", Value: "unknown"}}),
|
|
{Label: "신호 / 검색어", Name: "keyword", Type: "text", Placeholder: "DCD, DSR, CTS 또는 메시지"},
|
|
{Label: "시작 시간", Name: "from", Type: "datetime-local"},
|
|
{Label: "종료 시간", Name: "to", Type: "datetime-local"},
|
|
selectOptionsField("페이지당 표시", "limit", "100", limitOptions),
|
|
selectField("다운로드 형식", "export_format", "CSV", []string{"CSV", "JSON", "JSONL"}),
|
|
selectField("최대 다운로드 건수", "export_limit", "1000", []string{"100", "500", "1000", "5000"}),
|
|
toggleField("자동 갱신 (5초)", "auto_refresh", true),
|
|
},
|
|
Notes: []string{"장비 하드웨어가 제공하지 않는 DCD/DSR 신호는 기록할 수 없습니다.", "수신 키워드는 포트 로그의 검색어로 조회할 수 있습니다."},
|
|
},
|
|
},
|
|
{
|
|
Kind: "table",
|
|
Title: "포트 상태 이벤트",
|
|
Subtitle: "상태 변경 기록만 조회·내보내기합니다.",
|
|
Table: &TableData{
|
|
Columns: []string{"시간", "포트", "신호", "상태 변화", "유형", "내용"},
|
|
EmptyMessage: "조건에 맞는 포트 상태 이벤트가 없습니다.",
|
|
PrimaryLabel: "새로고침",
|
|
SecondaryLabel: "로그 다운로드",
|
|
},
|
|
},
|
|
}), true
|
|
default:
|
|
return PageData{}, false
|
|
}
|
|
}
|
|
|
|
func systemLogSettingsTabs(path string) []SubTab {
|
|
return []SubTab{
|
|
{Label: "로그 서버", Path: "/settings/system-log/remote", Active: path == "/settings/system-log/remote"},
|
|
{Label: "로그 이벤트", Path: "/settings/system-log/events", Active: path == "/settings/system-log/events" || path == "/settings/system-log/status-event"},
|
|
}
|
|
}
|
|
|
|
func portStatusPage() PageData {
|
|
return newPage("/port-status", "포트 상태", "포트 상태", []string{"포트 상태"}, nil, []ContentBlock{
|
|
{
|
|
Kind: "table",
|
|
Title: "포트 상태 요약",
|
|
Table: &TableData{
|
|
Columns: []string{"포트", "상태", "프로토콜", "세션", "RX/TX"},
|
|
EmptyMessage: "데이터가 없습니다.",
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func passwordPage() PageData {
|
|
return newPage("/password", "비밀번호 변경", "비밀번호 변경", []string{"비밀번호 변경"}, nil, []ContentBlock{
|
|
formBlock("비밀번호 변경", "change-password", []FormField{
|
|
{Label: "사용자 이름", Name: "username", Type: "text", Value: "terauser", Disabled: true},
|
|
passwordField("기존 비밀번호", "oldPassword", ""),
|
|
passwordField("새 비밀번호", "newPassword", ""),
|
|
passwordField("비밀번호 확인", "confirmPassword", ""),
|
|
}, "변경", ""),
|
|
})
|
|
}
|
|
|
|
func actionPage(path string, title string, description string, action string) PageData {
|
|
return newPage(path, title, title, []string{title}, nil, []ContentBlock{
|
|
{
|
|
Kind: "action",
|
|
Title: title,
|
|
Action: &ActionPanel{
|
|
Description: description,
|
|
Buttons: []ActionButton{
|
|
{Label: "실행", Action: action, Variant: "primary"},
|
|
{Label: "취소", Action: "cancel", Variant: "secondary"},
|
|
},
|
|
},
|
|
},
|
|
modalBlock(title+" 확인", "이 작업은 백엔드 API를 통해 장비 구성에 반영됩니다.", []FormField{
|
|
textAreaField("안내", "note", "작업 전 현재 구성과 서비스 상태를 확인하세요.", 4),
|
|
}),
|
|
})
|
|
}
|
|
|
|
func simpleFormPage(path string, section string, breadcrumbs []string, title string, fields []FormField) PageData {
|
|
return newPage(path, section, title, breadcrumbs, nil, []ContentBlock{
|
|
formBlock(title, "apply-"+sanitizePath(path), fields, "적용", ""),
|
|
})
|
|
}
|
|
|
|
func tablePage(path string, title string, breadcrumbs []string, columns []string, rows []TableRow, subtabs []SubTab) PageData {
|
|
return newPage(path, title, title, breadcrumbs, subtabs, []ContentBlock{
|
|
tableBlock(title, columns, rows, "추가", "변경"),
|
|
})
|
|
}
|
|
|
|
func newPortPage(path string, portID int, subtabs []SubTab, blocks []ContentBlock) PageData {
|
|
return newPage(path, "포트 설정", fmt.Sprintf("포트 설정 / Port %d", portID), []string{"포트 설정", fmt.Sprintf("Port %d", portID)}, subtabs, blocks)
|
|
}
|
|
|
|
func newPage(path string, section string, title string, breadcrumbs []string, subtabs []SubTab, blocks []ContentBlock) PageData {
|
|
return PageData{
|
|
AppName: "TERACLONE",
|
|
MockMode: config.MockMode,
|
|
CurrentPath: path,
|
|
Title: title,
|
|
Section: section,
|
|
Breadcrumbs: breadcrumbs,
|
|
WorkspaceTabs: workspaceTabs(path, title),
|
|
Menu: menu(path),
|
|
SubTabs: subtabs,
|
|
Blocks: blocks,
|
|
}
|
|
}
|
|
|
|
func ApplyRole(page PageData, role string) PageData {
|
|
page.CurrentRole = strings.ToLower(strings.TrimSpace(role))
|
|
if page.CurrentRole != "port_operator" {
|
|
return page
|
|
}
|
|
allowed := map[string]bool{"대시보드": true, "포트 설정": true, "포트 상태": true}
|
|
filtered := make([]MenuGroup, 0, 3)
|
|
for _, group := range page.Menu {
|
|
if allowed[group.Label] {
|
|
filtered = append(filtered, group)
|
|
}
|
|
}
|
|
filtered = append(filtered, MenuGroup{Label: "포트 로그", Path: "/logs/ports"})
|
|
if page.CurrentPath == "/logs/ports" {
|
|
page.SubTabs = []SubTab{{Label: "포트 로그", Path: "/logs/ports", Active: true}}
|
|
}
|
|
page.Menu = filtered
|
|
return page
|
|
}
|
|
|
|
func formBlock(title string, action string, fields []FormField, primary string, secondary string) ContentBlock {
|
|
return ContentBlock{
|
|
Kind: "form",
|
|
Title: title,
|
|
Form: &FormData{
|
|
Action: "#",
|
|
PrimaryLabel: primary,
|
|
SecondaryLabel: secondary,
|
|
Fields: fields,
|
|
},
|
|
}
|
|
}
|
|
|
|
func groupedField(field FormField, group string, hidden bool) FormField {
|
|
field.Group = group
|
|
field.Hidden = hidden
|
|
return field
|
|
}
|
|
|
|
func tableBlock(title string, columns []string, rows []TableRow, primary string, secondary string) ContentBlock {
|
|
return ContentBlock{
|
|
Kind: "table",
|
|
Title: title,
|
|
Table: &TableData{
|
|
Columns: columns,
|
|
Rows: rows,
|
|
EmptyMessage: "데이터가 없습니다.",
|
|
PrimaryLabel: primary,
|
|
SecondaryLabel: secondary,
|
|
},
|
|
}
|
|
}
|
|
|
|
func modalBlock(title string, message string, fields []FormField) ContentBlock {
|
|
return ContentBlock{
|
|
Kind: "modal",
|
|
Title: title,
|
|
Modal: &ModalData{
|
|
Root: sanitizePath(title),
|
|
Title: title,
|
|
Message: message,
|
|
Fields: fields,
|
|
Buttons: []ActionButton{
|
|
{Label: "확인", Action: "confirm-modal", Variant: "primary"},
|
|
{Label: "취소", Action: "cancel-modal", Variant: "secondary"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func userAccountModalBlock() ContentBlock {
|
|
return ContentBlock{
|
|
Kind: "modal",
|
|
Title: "사용자 추가",
|
|
Modal: &ModalData{
|
|
Root: "user-create",
|
|
Title: "사용자 추가",
|
|
Message: "장비의 Local 인증 저장소에 사용자를 추가합니다.",
|
|
Fields: []FormField{
|
|
{Type: "hidden", Name: "id", Value: ""},
|
|
{Label: "사용자 이름", Name: "username", Type: "text", Required: true, Placeholder: "영문 소문자, 숫자, 점, 하이픈, 밑줄", Hint: "최대 64자이며 생성 후에는 변경할 수 없습니다."},
|
|
{Label: "비밀번호", Name: "password", Type: "password", Required: true, Placeholder: "8자 이상 입력", Hint: "수정할 때 비워두면 기존 비밀번호를 유지합니다."},
|
|
{
|
|
Label: "그룹", Name: "group", Type: "select", Required: true, Value: "administrators",
|
|
Options: []Option{
|
|
{Label: "Administrators (관리자)", Value: "administrators", Selected: true},
|
|
{Label: "Port operators (포트 운영자)", Value: "port_operators"},
|
|
},
|
|
},
|
|
{
|
|
Label: "계정 상태", Name: "enabled", Type: "select", Value: "true",
|
|
Options: []Option{{Label: "사용", Value: "true", Selected: true}, {Label: "중지", Value: "false"}},
|
|
},
|
|
},
|
|
Buttons: []ActionButton{
|
|
{Label: "저장", Action: "submit-user-form", Variant: "primary"},
|
|
{Label: "취소", Action: "cancel-modal", Variant: "secondary"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func addressFilterListPage(path string) PageData {
|
|
filterName := "IPv4 필터"
|
|
if strings.Contains(path, "ipv6") {
|
|
filterName = "IPv6 필터"
|
|
}
|
|
if strings.Contains(path, "mac") {
|
|
filterName = "MAC 필터"
|
|
}
|
|
|
|
return newPage(path, "관리자 운영", filterName, []string{"관리자 운영", "주소 필터", filterName}, addressFilterTabs(path), []ContentBlock{
|
|
tableBlock(filterName, []string{"규칙 ID", "주소", "프로토콜", "포트", "상태", "관리"}, nil, "규칙 추가", ""),
|
|
})
|
|
}
|
|
|
|
func addressFilterNewPage(path string) PageData {
|
|
title := "IPv4 필터 추가"
|
|
fields := []FormField{
|
|
fixedTextField("규칙 ID", "name", "주소와 프로토콜로 자동 생성"),
|
|
textField("IPv4 주소/CIDR", "address", "192.168.0.0/24", true),
|
|
selectOptionsField("프로토콜", "protocol", "all", protocolOptions()),
|
|
fixedTextField("목적지 포트", "port", "0"),
|
|
toggleField("규칙 사용", "enabled", true),
|
|
}
|
|
if strings.Contains(path, "ipv6") {
|
|
title = "IPv6 필터 추가"
|
|
fields = []FormField{
|
|
fixedTextField("규칙 ID", "name", "주소와 프로토콜로 자동 생성"),
|
|
textField("IPv6 주소/CIDR", "address", "2001:db8::/64", true),
|
|
selectOptionsField("프로토콜", "protocol", "all", protocolOptions()),
|
|
fixedTextField("목적지 포트", "port", "0"),
|
|
toggleField("규칙 사용", "enabled", true),
|
|
}
|
|
}
|
|
if strings.Contains(path, "mac") {
|
|
title = "MAC 필터 추가"
|
|
fields = []FormField{
|
|
fixedTextField("규칙 ID", "name", "주소와 프로토콜로 자동 생성"),
|
|
textField("MAC 주소", "address", "00:11:22:33:44:55", true),
|
|
selectOptionsField("프로토콜", "protocol", "all", protocolOptions()),
|
|
fixedTextField("목적지 포트", "port", "0"),
|
|
toggleField("규칙 사용", "enabled", true),
|
|
}
|
|
}
|
|
|
|
return newPage(path, "관리자 운영", title, []string{"관리자 운영", "주소 필터", title}, addressFilterTabs(path), []ContentBlock{formBlock(title, "address-filter-add", fields, "추가", "")})
|
|
}
|
|
|
|
func authServerTabs(path string) []SubTab {
|
|
return []SubTab{{"RADIUS", "/admin/auth/radius", path == "/admin/auth/radius"}, {"TACACS+", "/admin/auth/tacacs", path == "/admin/auth/tacacs"}}
|
|
}
|
|
|
|
func addressFilterTabs(path string) []SubTab {
|
|
return []SubTab{
|
|
{"기본", "/admin/address-filter", path == "/admin/address-filter"},
|
|
{"IPv4", "/admin/address-filter/ipv4", strings.Contains(path, "/ipv4")},
|
|
{"MAC", "/admin/address-filter/mac", strings.Contains(path, "/mac")},
|
|
}
|
|
}
|
|
|
|
func sslTabs(path string) []SubTab {
|
|
return []SubTab{
|
|
{"인증서", "/admin/ssl", path == "/admin/ssl"},
|
|
{"새로 만들기", "/admin/ssl/new", path == "/admin/ssl/new"},
|
|
{"가져오기", "/admin/ssl/import", path == "/admin/ssl/import"},
|
|
{"CSR 내보내기", "/admin/ssl/import-csr", path == "/admin/ssl/import-csr"},
|
|
}
|
|
}
|
|
|
|
func backupTabs(path string) []SubTab {
|
|
return []SubTab{{"내보내기", "/admin/backup/export", path == "/admin/backup/export"}, {"복원", "/admin/backup/restore", path != "/admin/backup/export"}}
|
|
}
|
|
|
|
func alertTabs(path string) []SubTab {
|
|
return []SubTab{
|
|
{"시스템 이벤트", "/settings/alert/event", path == "/settings/alert/event"},
|
|
{"시리얼 이벤트", "/settings/alert/serial-event", path == "/settings/alert/serial-event"},
|
|
{"메일", "/settings/alert/mail", path == "/settings/alert/mail"},
|
|
{"SNMP Trap", "/settings/alert/snmp-trap-filter", path == "/settings/alert/snmp-trap-filter"},
|
|
}
|
|
}
|
|
|
|
func roleOptions() []Option {
|
|
return []Option{{Label: "포트 운영자", Value: "port_operator", Selected: true}, {Label: "관리자", Value: "administrator"}}
|
|
}
|
|
|
|
func protocolOptions() []Option {
|
|
return []Option{{Label: "전체", Value: "all", Selected: true}, {Label: "HTTP", Value: "http"}, {Label: "HTTPS", Value: "https"}, {Label: "SSH", Value: "ssh"}, {Label: "Telnet", Value: "telnet"}}
|
|
}
|
|
|
|
func channelSelectOptions() []Option {
|
|
options := make([]Option, 0, 48)
|
|
for channel := 1; channel <= 48; channel++ {
|
|
value := fmt.Sprintf("CH%02d", channel)
|
|
options = append(options, Option{Label: value, Value: value, Selected: channel == 1})
|
|
}
|
|
return options
|
|
}
|
|
|
|
func workspaceTabs(path string, title string) []WorkspaceTab {
|
|
return []WorkspaceTab{
|
|
{Key: tabKeyForPath(path), Label: tabLabelForPath(path, title), Path: path, Active: true},
|
|
}
|
|
}
|
|
|
|
func menu(path string) []MenuGroup {
|
|
groups := []MenuGroup{
|
|
{Label: "대시보드", Icon: "DB", Path: "/"},
|
|
{Label: "네트워크 설정", Icon: "NW", Path: "/network/interface-1"},
|
|
{Label: "포트 설정", Icon: "PT", Path: "/ports/1/parameters"},
|
|
{Label: "포트 디버그", Icon: "DG", Path: "/port-debug"},
|
|
{Label: "관리자 운영", Icon: "AD", Path: "/admin/users"},
|
|
{Label: "기타 설정", Icon: "ST", Path: "/settings/alert/event"},
|
|
{Label: "진단 도구", Icon: "TL", Path: "/tools/nettest/ping"},
|
|
{Label: "로그", Icon: "LG", Path: "/logs/system"},
|
|
{Label: "포트 상태", Icon: "PS", Path: "/port-status"},
|
|
{Label: "비밀번호 변경", Icon: "PW", Path: "/password"},
|
|
{Label: "공장 초기화", Icon: "FR", Path: "/device/factory-reset"},
|
|
}
|
|
|
|
for i := range groups {
|
|
if groups[i].Path == "/" {
|
|
groups[i].Active = path == "/"
|
|
continue
|
|
}
|
|
groups[i].Active = path == groups[i].Path || strings.HasPrefix(path, trimMenuRoot(groups[i].Path))
|
|
}
|
|
|
|
for i := range groups {
|
|
switch groups[i].Label {
|
|
case "포트 설정":
|
|
groups[i].Expanded = false
|
|
case "관리자 운영":
|
|
groups[i].Expanded = strings.HasPrefix(path, "/admin/")
|
|
groups[i].Children = append(groups[i].Children,
|
|
MenuChild{"사용자 계정", "/admin/users", strings.HasPrefix(path, "/admin/users")},
|
|
MenuChild{"인증 서버", "/admin/auth/radius", strings.HasPrefix(path, "/admin/auth/")},
|
|
MenuChild{"주소 필터", "/admin/address-filter", strings.HasPrefix(path, "/admin/address-filter")},
|
|
MenuChild{"SNMP", "/admin/snmp", path == "/admin/snmp"},
|
|
MenuChild{"SSL 인증서", "/admin/ssl", strings.HasPrefix(path, "/admin/ssl")},
|
|
MenuChild{"백업/복원", "/admin/backup/export", strings.HasPrefix(path, "/admin/backup/")},
|
|
MenuChild{"펌웨어", "/admin/firmware", path == "/admin/firmware"},
|
|
)
|
|
case "기타 설정":
|
|
groups[i].Expanded = strings.HasPrefix(path, "/settings/")
|
|
groups[i].Children = append(groups[i].Children,
|
|
MenuChild{"알림 설정", "/settings/alert/event", strings.HasPrefix(path, "/settings/alert/")},
|
|
MenuChild{"시스템 로그", "/settings/system-log/remote", strings.HasPrefix(path, "/settings/system-log/")},
|
|
MenuChild{"날짜와 시간", "/settings/datetime", path == "/settings/datetime"},
|
|
)
|
|
case "진단 도구":
|
|
groups[i].Expanded = strings.HasPrefix(path, "/tools/")
|
|
groups[i].Children = append(groups[i].Children,
|
|
MenuChild{"네트워크 테스트", "/tools/nettest/ping", strings.HasPrefix(path, "/tools/nettest/")},
|
|
MenuChild{"포트 별칭", "/tools/alias", path == "/tools/alias"},
|
|
)
|
|
case "로그":
|
|
groups[i].Expanded = strings.HasPrefix(path, "/logs/")
|
|
groups[i].Children = append(groups[i].Children,
|
|
MenuChild{"시스템 로그", "/logs/system", path == "/logs/system"},
|
|
MenuChild{"포트 로그", "/logs/ports", path == "/logs/ports"},
|
|
MenuChild{"포트 상태 이벤트", "/logs/links", path == "/logs/links"},
|
|
)
|
|
}
|
|
}
|
|
|
|
return groups
|
|
}
|
|
|
|
func portLinks() []PortLink {
|
|
links := make([]PortLink, 0, 48)
|
|
for i := 1; i <= 48; i++ {
|
|
links = append(links, PortLink{
|
|
Number: i,
|
|
// The real state is populated by /api/v1/ports. Rendering guessed
|
|
// values here causes a visible false-UP flash during page changes.
|
|
Up: false,
|
|
Blink: false,
|
|
})
|
|
}
|
|
return links
|
|
}
|
|
|
|
func portTargets(selected int) []PortTarget {
|
|
targets := make([]PortTarget, 0, 48)
|
|
for i := 1; i <= 48; i++ {
|
|
targets = append(targets, PortTarget{
|
|
Number: i,
|
|
Checked: i == selected,
|
|
})
|
|
}
|
|
return targets
|
|
}
|
|
|
|
func textField(label string, name string, value string, required bool) FormField {
|
|
return FormField{Label: label, Name: name, Type: "text", Value: value, Required: required}
|
|
}
|
|
|
|
func shortcutField(label, name, value, hint string) FormField {
|
|
// Shortcut values remain in the form so the integration layer can preserve
|
|
// the running configuration, but they are intentionally not user-facing.
|
|
return FormField{Label: label, Name: name, Type: "text", Value: value, Hidden: true}
|
|
}
|
|
|
|
func numberField(label string, name string, value string, required bool) FormField {
|
|
return boundedNumberField(label, name, value, required, "1", "20")
|
|
}
|
|
|
|
func boundedNumberField(label string, name string, value string, required bool, min string, max string) FormField {
|
|
return FormField{Label: label, Name: name, Type: "number", Value: value, Required: required, Min: min, Max: max}
|
|
}
|
|
|
|
func fixedTextField(label string, name string, value string) FormField {
|
|
return FormField{Label: label, Name: name, Type: "text", Value: value, Disabled: true, Fixed: true}
|
|
}
|
|
|
|
func passwordField(label string, name string, value string) FormField {
|
|
return FormField{Label: label, Name: name, Type: "password", Placeholder: value}
|
|
}
|
|
|
|
func fileField(label string, name string, accept string, required bool) FormField {
|
|
return FormField{Label: label, Name: name, Type: "file", Accept: accept, Required: required}
|
|
}
|
|
|
|
func textAreaField(label string, name string, value string, rows int) FormField {
|
|
return FormField{Label: label, Name: name, Type: "textarea", Value: value, Rows: rows}
|
|
}
|
|
|
|
func toggleField(label string, name string, checked bool) FormField {
|
|
return FormField{Label: label, Name: name, Type: "toggle", Checked: checked}
|
|
}
|
|
|
|
func selectField(label string, name string, value string, options []string) FormField {
|
|
field := FormField{Label: label, Name: name, Type: "select", Value: value}
|
|
for _, option := range options {
|
|
field.Options = append(field.Options, Option{
|
|
Label: option,
|
|
Value: option,
|
|
Selected: option == value,
|
|
})
|
|
}
|
|
return field
|
|
}
|
|
|
|
func selectOptionsField(label string, name string, value string, options []Option) FormField {
|
|
field := FormField{Label: label, Name: name, Type: "select", Value: value}
|
|
for _, option := range options {
|
|
option.Selected = option.Value == value
|
|
field.Options = append(field.Options, option)
|
|
}
|
|
return field
|
|
}
|
|
|
|
func portModeField(value string) FormField {
|
|
return selectOptionsField("동작 모드", "mode", value, []Option{
|
|
{Label: "사용 안 함", Value: "disabled"},
|
|
{Label: "Reverse SSH", Value: "reverse_ssh"},
|
|
{Label: "Reverse Telnet", Value: "reverse_telnet"},
|
|
})
|
|
}
|
|
|
|
func authenticationField() FormField {
|
|
return selectOptionsField("인증 방식", "authentication", "local", []Option{
|
|
{Label: "Local", Value: "local"},
|
|
{Label: "RADIUS", Value: "radius"},
|
|
{Label: "TACACS", Value: "tacacs"},
|
|
})
|
|
}
|
|
|
|
func sanitizePath(path string) string {
|
|
return strings.NewReplacer("/", "-", ":", "-", "_", "-").Replace(strings.Trim(path, "/"))
|
|
}
|
|
|
|
func trimMenuRoot(path string) string {
|
|
if path == "/" {
|
|
return "/"
|
|
}
|
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
if len(parts) == 0 {
|
|
return "/"
|
|
}
|
|
return "/" + parts[0] + "/"
|
|
}
|
|
|
|
func shortLabelForPath(path string) string {
|
|
switch {
|
|
case strings.HasPrefix(path, "/network/"):
|
|
return "네트워크 설정"
|
|
case strings.HasPrefix(path, "/ports/"):
|
|
return "포트 설정"
|
|
case strings.HasPrefix(path, "/admin/"):
|
|
return "관리자 운영"
|
|
case strings.HasPrefix(path, "/settings/"):
|
|
return "기타 설정"
|
|
case strings.HasPrefix(path, "/tools/"):
|
|
return "진단 도구"
|
|
case strings.HasPrefix(path, "/logs/"):
|
|
return "로그"
|
|
default:
|
|
return "장비 화면"
|
|
}
|
|
}
|
|
|
|
func tabLabelForPath(path string, title string) string {
|
|
switch {
|
|
case path == "/":
|
|
return "대시보드"
|
|
case strings.HasPrefix(path, "/ports/"):
|
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
if len(parts) >= 2 {
|
|
if portID, err := strconv.Atoi(parts[1]); err == nil {
|
|
return fmt.Sprintf("포트 %d 설정", portID)
|
|
}
|
|
}
|
|
case strings.HasPrefix(path, "/admin/snmp"):
|
|
return "SNMP"
|
|
case strings.HasPrefix(path, "/tools/nettest/"):
|
|
return "네트워크 테스트"
|
|
}
|
|
|
|
if title != "" {
|
|
return title
|
|
}
|
|
|
|
return shortLabelForPath(path)
|
|
}
|
|
|
|
func tabKeyForPath(path string) string {
|
|
switch {
|
|
case path == "/":
|
|
return "dashboard"
|
|
case strings.HasPrefix(path, "/network/"):
|
|
return "network"
|
|
case strings.HasPrefix(path, "/ports/"):
|
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
if len(parts) >= 2 {
|
|
if portID, err := strconv.Atoi(parts[1]); err == nil {
|
|
return fmt.Sprintf("port-%d", portID)
|
|
}
|
|
}
|
|
return "ports"
|
|
case strings.HasPrefix(path, "/port-debug"):
|
|
return "port-debug"
|
|
case strings.HasPrefix(path, "/admin/snmp"):
|
|
return "admin-snmp"
|
|
case strings.HasPrefix(path, "/admin/"):
|
|
return "admin"
|
|
case strings.HasPrefix(path, "/settings/"):
|
|
return "settings"
|
|
case strings.HasPrefix(path, "/tools/nettest/"):
|
|
return "network-test"
|
|
case strings.HasPrefix(path, "/tools/"):
|
|
return "tools"
|
|
case strings.HasPrefix(path, "/logs/"):
|
|
return "logs"
|
|
case path == "/port-status":
|
|
return "port-status"
|
|
case path == "/password":
|
|
return "password"
|
|
case path == "/config/save":
|
|
return "config-save"
|
|
case path == "/ports/restart":
|
|
return "ports-restart"
|
|
case path == "/device/restart":
|
|
return "device-restart"
|
|
default:
|
|
return strings.Trim(strings.ReplaceAll(path, "/", "-"), "-")
|
|
}
|
|
}
|
|
|