Localize German, French, and Korean demo content and capture workflows. Preserve real software keyboards and invariant numeric parsing during captures. Sort language choices by code and update the published documentation and marketing asset submodules.
296 lines
14 KiB
Swift
296 lines
14 KiB
Swift
import Foundation
|
|
import TermindApplication
|
|
import TermindDomain
|
|
|
|
import struct TermindDomain.Host
|
|
|
|
// MARK: - Demo data
|
|
|
|
extension HostLibraryModel {
|
|
/// Seed data for P0, so the sidebar and workspace list can be exercised before any
|
|
/// persistence or networking exists.
|
|
static func demo(language: AppLanguage = .english, includesConnectionActivity: Bool = true) -> HostLibraryModel {
|
|
func text(
|
|
_ en: String, _ zhHans: String, _ zhHant: String, _ ja: String, _ ko: String, _ de: String, _ fr: String
|
|
) -> String {
|
|
switch language {
|
|
case .simplifiedChinese: zhHans
|
|
case .traditionalChinese: zhHant
|
|
case .japanese: ja
|
|
case .korean: ko
|
|
case .german: de
|
|
case .french: fr
|
|
case .system, .english: en
|
|
}
|
|
}
|
|
func distribution(_ id: String) -> DetectedDistribution {
|
|
guard let distribution = DetectedDistribution(id: id) else {
|
|
preconditionFailure("Invalid bundled Demo distribution: \(id)")
|
|
}
|
|
return distribution
|
|
}
|
|
|
|
let production = HostGroup(
|
|
name: text("Production", "生产环境", "正式環境", "本番環境", "운영 환경", "Produktion", "Production"), sortIndex: 0)
|
|
let staging = HostGroup(
|
|
name: text("Staging", "预发布环境", "測試環境", "ステージング", "스테이징", "Staging", "Préproduction"), sortIndex: 1)
|
|
let homelab = HostGroup(
|
|
name: text("Homelab", "家庭实验室", "家庭實驗室", "ホームラボ", "홈랩", "Heimlabor", "Labo personnel"), sortIndex: 2)
|
|
let edge = HostGroup(
|
|
name: text("Edge", "边缘节点", "邊緣節點", "エッジ", "에지", "Edge", "Périphérie"), parentID: production.id, sortIndex: 0
|
|
)
|
|
|
|
let now = Date(timeIntervalSinceReferenceDate: 776_000_000)
|
|
|
|
let personalKey = SSHKeyRef(
|
|
label: text("Personal", "个人", "個人", "個人用", "개인용", "Privat", "Personnel"), algorithm: .ed25519,
|
|
fingerprint: "SHA256:9pKQ2vC0nJ7hR4mXbL1sT8yWfE3dA6uZgN5qOiVxHkM",
|
|
credentialRef: "termind.key.personal", colorTag: .blue
|
|
)
|
|
let workKey = SSHKeyRef(
|
|
label: text(
|
|
"Work (no sync)", "工作(不同步)", "工作(不同步)", "仕事用(同期なし)", "업무용 (동기화 안 함)", "Arbeit (ohne Sync)",
|
|
"Travail (sans synchro)"), algorithm: .ecdsa256,
|
|
fingerprint: "SHA256:3tBn8xLpQ2wZ7yKvR1mHcE9dS4fJ6uA0gT5iNqOxVbY",
|
|
credentialRef: "termind.key.work",
|
|
storage: .thisDevice, colorTag: .purple
|
|
)
|
|
|
|
// Built before the hosts, because a host now references its login by id.
|
|
// One of each shape, so all four combinations of the two secrets are on
|
|
// screen at P0: key only, key *and* password, password only, neither.
|
|
let deploy = Identity(
|
|
name: text("Deploy", "部署账号", "部署帳號", "デプロイ", "배포", "Deployment", "Déploiement"), username: "deploy",
|
|
keyID: personalKey.id,
|
|
notes: text(
|
|
"Production web tier", "生产环境 Web 层", "正式環境 Web 層", "本番環境のWeb層", "운영 웹 계층", "Produktive Webserver",
|
|
"Serveurs web de production"),
|
|
colorTag: .green
|
|
)
|
|
let postgres = Identity(
|
|
name: text(
|
|
"Postgres admin", "Postgres 管理员", "Postgres 管理員", "Postgres管理者", "Postgres 관리자", "Postgres-Admin",
|
|
"Admin Postgres"), username: "postgres",
|
|
credentialRef: "termind.password.postgres", keyID: workKey.id,
|
|
notes: text(
|
|
"Key to get in, password for sudo", "使用密钥登录,密码用于 sudo", "使用金鑰登入,密碼用於 sudo", "ログインには鍵、sudoにはパスワード",
|
|
"키로 로그인, sudo에는 암호 사용", "Schlüssel zum Anmelden, Passwort für sudo",
|
|
"Clé pour la connexion, mot de passe pour sudo"),
|
|
colorTag: .orange
|
|
)
|
|
let consoleRoot = Identity(
|
|
name: text(
|
|
"Console root", "控制台 root", "主控台 root", "コンソールのroot", "콘솔 root", "Konsolen-root", "Root console"),
|
|
username: "root",
|
|
credentialRef: "termind.password.console-root",
|
|
notes: text(
|
|
"Out-of-band only", "仅限带外管理", "僅限頻外管理", "帯域外管理専用", "대역 외 관리 전용", "Nur Out-of-Band",
|
|
"Gestion hors bande uniquement"), colorTag: .red
|
|
)
|
|
let unfinished = Identity(
|
|
name: text("New admin", "新管理员", "新增管理員", "新しい管理者", "새 관리자", "Neuer Admin", "Nouvel admin"),
|
|
username: "admin",
|
|
notes: text(
|
|
"No secret chosen yet", "尚未选择凭据", "尚未選擇認證資訊", "シークレットは未選択", "아직 자격 증명을 선택하지 않았어요",
|
|
"Noch keine Zugangsdaten gewählt", "Aucun secret sélectionné")
|
|
)
|
|
|
|
// Half the fleet is left without an identity on purpose, so the "not set
|
|
// yet" state is visible at P0 instead of first appearing in the field.
|
|
//
|
|
// A fresh install has no connection history, so Recents starts empty and the sidebar shows
|
|
// its placeholder; seeding a history here put that empty state out of
|
|
// reach and told a first launch it had already been on production.
|
|
// `AppModel.open` fills the list the moment anything is opened.
|
|
let hosts: [Host] = [
|
|
Host(
|
|
name: "web-01", host: "10.0.1.5", identityID: deploy.id,
|
|
groupID: production.id, tags: ["nginx", "prod"], colorTag: .red,
|
|
detectedDistribution: distribution("ubuntu"),
|
|
sortIndex: 0
|
|
),
|
|
Host(
|
|
name: "web-02", host: "10.0.1.6", identityID: deploy.id,
|
|
groupID: production.id, tags: ["nginx", "prod"], colorTag: .red,
|
|
detectedDistribution: distribution("debian"),
|
|
sortIndex: 1
|
|
),
|
|
Host(
|
|
name: "db-primary", host: "10.0.2.10", identityID: postgres.id,
|
|
groupID: production.id, tags: ["postgres"], colorTag: .purple,
|
|
detectedDistribution: distribution("rocky"),
|
|
sortIndex: 2
|
|
),
|
|
Host(
|
|
name: "cdn-edge-tokyo", host: "203.0.113.7", port: 2222, identityID: consoleRoot.id,
|
|
groupID: edge.id, tags: ["varnish", "apac"], colorTag: .orange,
|
|
detectedDistribution: distribution("alpine"),
|
|
sortIndex: 0
|
|
),
|
|
Host(
|
|
name: "staging-app", host: "staging.internal",
|
|
groupID: staging.id, tags: ["staging"], colorTag: .yellow,
|
|
detectedDistribution: distribution("fedora"),
|
|
sortIndex: 0
|
|
),
|
|
Host(
|
|
name: "nas", host: "192.168.1.20",
|
|
groupID: homelab.id, tags: ["zfs"], colorTag: .teal,
|
|
detectedDistribution: distribution("nixos"), sortIndex: 0
|
|
),
|
|
Host(
|
|
name: "pi-hole", host: "192.168.1.21",
|
|
groupID: homelab.id, tags: ["dns"], colorTag: .green,
|
|
detectedDistribution: distribution("raspberrypios"), sortIndex: 1
|
|
),
|
|
Host(
|
|
name: "scratch-box", host: "172.16.0.9",
|
|
detectedDistribution: distribution("opensuse-tumbleweed"),
|
|
sortIndex: 0
|
|
),
|
|
]
|
|
|
|
let diagnostics = SnippetPackage(
|
|
name: text("Diagnostics", "诊断", "診斷", "診断", "진단", "Diagnose", "Diagnostic"), sortIndex: 0
|
|
)
|
|
let webOperations = SnippetPackage(
|
|
name: text("Web Operations", "Web 运维", "Web 維運", "Web運用", "웹 운영", "Webbetrieb", "Exploitation web"),
|
|
parentID: diagnostics.id, sortIndex: 0
|
|
)
|
|
let snippets = [
|
|
Snippet(
|
|
title: text(
|
|
"Disk usage by directory", "按目录查看磁盘占用", "依目錄查看磁碟用量", "ディレクトリ別のディスク使用量", "디렉터리별 디스크 사용량",
|
|
"Speicherplatz pro Verzeichnis", "Espace disque par dossier"),
|
|
body: "du -sh /* | sort -h",
|
|
packageID: diagnostics.id, targetHostIDs: [hosts[0].id],
|
|
sortIndex: 0
|
|
),
|
|
Snippet(
|
|
title: text(
|
|
"Top memory consumers", "内存占用最高的进程", "記憶體用量最高的程序", "メモリ使用量の多いプロセス", "메모리 사용량이 많은 프로세스",
|
|
"Größte RAM-Verbraucher", "Processus les plus gourmands en mémoire"),
|
|
body: "ps aux --sort=-%mem | head -20",
|
|
packageID: diagnostics.id,
|
|
targetHostIDs: [hosts[0].id, hosts[1].id],
|
|
sortIndex: 1
|
|
),
|
|
Snippet(
|
|
title: text(
|
|
"Tail nginx errors", "持续查看 Nginx 错误", "持續查看 Nginx 錯誤", "nginxのエラーを追跡", "nginx 오류 추적",
|
|
"nginx-Fehler verfolgen", "Suivre les erreurs nginx"),
|
|
body: "tail -f /var/log/nginx/error.log",
|
|
packageID: webOperations.id,
|
|
targetHostIDs: [hosts[0].id, hosts[1].id],
|
|
sortIndex: 0
|
|
),
|
|
Snippet(
|
|
title: text(
|
|
"Listening ports", "监听中的端口", "監聽中的連接埠", "待ち受けポート", "수신 대기 포트", "Lauschende Ports", "Ports en écoute"
|
|
),
|
|
body: "ss -tulpn", sortIndex: 3
|
|
),
|
|
]
|
|
|
|
let knownHosts = [
|
|
KnownHostEntry(
|
|
hostname: "10.0.1.5", algorithm: .ed25519,
|
|
fingerprint: "SHA256:kL9mR2xQ7vB4nT1yC6dW8sF3jH0aP5uZeG2iXoNbVrM",
|
|
addedAt: now.addingTimeInterval(-2_592_000)
|
|
),
|
|
KnownHostEntry(
|
|
hostname: "10.0.2.10", algorithm: .ed25519,
|
|
fingerprint: "SHA256:7wQxN3bV9cL2mK8pR1tY6sD4fG0hJ5uZaE2iXoBnVrT",
|
|
addedAt: now.addingTimeInterval(-1_209_600)
|
|
),
|
|
KnownHostEntry(
|
|
hostname: "203.0.113.7", port: 2222, algorithm: .ecdsa256,
|
|
fingerprint: "SHA256:2mF8sK1pQ7xR4vB9nL3yT6wC0dH5jA2uZgE7iOxNbVr",
|
|
addedAt: now.addingTimeInterval(-604_800)
|
|
),
|
|
KnownHostEntry(
|
|
hostname: "staging.internal", algorithm: .rsa,
|
|
fingerprint: "SHA256:5vT2nB8xQ1mL7kR4pW9yC3sD6fH0jG5uZaE8iXoNbVr",
|
|
addedAt: now.addingTimeInterval(-15_552_000)
|
|
),
|
|
]
|
|
|
|
let web01 = hosts[0]
|
|
let db = hosts[2]
|
|
let portForwardings = [
|
|
PortForwardingRule(
|
|
hostID: db.id, kind: .local, bindPort: 5432,
|
|
targetHost: "localhost", targetPort: 5432,
|
|
label: text("Postgres", "Postgres 数据库", "Postgres 資料庫", "Postgres", "Postgres", "Postgres", "Postgres")
|
|
),
|
|
PortForwardingRule(
|
|
hostID: web01.id, kind: .dynamic, bindPort: 1080,
|
|
label: text(
|
|
"SOCKS proxy", "SOCKS 代理", "SOCKS Proxy", "SOCKSプロキシ", "SOCKS 프록시", "SOCKS-Proxy", "Proxy SOCKS")
|
|
),
|
|
]
|
|
|
|
// A queue with one of each state, so Batch Startup, progress, and the
|
|
// failure row can all be checked without a server.
|
|
let sftpTransfers = [
|
|
SFTPTransfer(
|
|
hostID: web01.id, direction: .upload,
|
|
localPath: "~/Builds/termind-1.0.tar.gz", remotePath: "/srv/releases/termind-1.0.tar.gz",
|
|
byteCount: 48_312_320, transferredBytes: 21_495_808, state: .running,
|
|
queuedAt: now.addingTimeInterval(-120)
|
|
),
|
|
SFTPTransfer(
|
|
hostID: db.id, direction: .download,
|
|
localPath: "~/Downloads/dump-2026-08-21.sql", remotePath: "/var/backups/dump-2026-08-21.sql",
|
|
byteCount: 1_073_741_824, state: .queued,
|
|
queuedAt: now.addingTimeInterval(-90)
|
|
),
|
|
SFTPTransfer(
|
|
hostID: web01.id, direction: .upload,
|
|
localPath: "~/Config/nginx.conf", remotePath: "/etc/nginx/nginx.conf",
|
|
byteCount: 8_192,
|
|
state: .failed(
|
|
text(
|
|
"Permission denied", "权限被拒绝", "權限遭拒", "アクセスが拒否されました", "권한이 거부되었어요", "Zugriff verweigert",
|
|
"Permission refusée")),
|
|
queuedAt: now.addingTimeInterval(-3_600)
|
|
),
|
|
SFTPTransfer(
|
|
hostID: db.id, direction: .download,
|
|
localPath: "~/Logs/pg.log", remotePath: "/var/log/postgresql/pg.log",
|
|
byteCount: 2_097_152, transferredBytes: 2_097_152, state: .completed,
|
|
queuedAt: now.addingTimeInterval(-7_200)
|
|
),
|
|
]
|
|
|
|
let store = HostLibraryModel(
|
|
hosts: hosts,
|
|
groups: [production, staging, homelab, edge],
|
|
keys: [personalKey, workKey],
|
|
identities: [deploy, postgres, consoleRoot, unfinished],
|
|
snippets: snippets,
|
|
snippetPackages: [diagnostics, webOperations],
|
|
portForwardings: portForwardings,
|
|
sftpTransfers: includesConnectionActivity ? sftpTransfers : [],
|
|
knownHosts: knownHosts
|
|
)
|
|
|
|
guard includesConnectionActivity else { return store }
|
|
|
|
// A spread of connection states so Vault and Welcome Host cards can be
|
|
// checked against every dot color without a server.
|
|
store.setStatus(.connected, for: web01.id)
|
|
store.setStatus(.connecting, for: db.id)
|
|
if let staging = hosts.first(where: { $0.name == "staging-app" }) {
|
|
store.setStatus(
|
|
.failed(
|
|
text(
|
|
"Connection refused", "连接被拒绝", "連線遭拒", "接続が拒否されました", "연결이 거부되었어요", "Verbindung abgelehnt",
|
|
"Connexion refusée")), for: staging.id
|
|
)
|
|
}
|
|
|
|
return store
|
|
}
|
|
}
|