ovtools 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -2
  3. package/assets/common/bond-inspect.sh +368 -0
  4. package/assets/common/config.sh +263 -0
  5. package/assets/common/extract-data.sh +197 -0
  6. package/assets/common/harden-host.sh +37 -0
  7. package/assets/common/install-deps.sh +93 -0
  8. package/assets/common/node-enroll.sh +65 -0
  9. package/assets/common/node-setup.sh +150 -0
  10. package/assets/common/node-start.sh +90 -0
  11. package/assets/common/node-stop.sh +18 -0
  12. package/assets/common/node-upgrade.sh +45 -0
  13. package/assets/common/pre-run.sh +497 -0
  14. package/assets/common/promote-validator.sh +104 -0
  15. package/assets/deploy/10-prepare-host.sh +98 -0
  16. package/assets/deploy/20-install-toolchain.sh +93 -0
  17. package/assets/deploy/30-setup-node.sh +82 -0
  18. package/assets/deploy/40-start-node.sh +75 -0
  19. package/assets/deploy/50-verify.sh +259 -0
  20. package/assets/deploy/60-enroll.sh +304 -0
  21. package/assets/deploy/70-promote.sh +152 -0
  22. package/assets/deploy/lib.sh +398 -0
  23. package/assets/manage/10-bond-increase.sh +149 -0
  24. package/assets/manage/10-diagnose-node.sh +169 -0
  25. package/assets/manage/20-exit-validator.sh +259 -0
  26. package/assets/manage/25-recover-node.sh +291 -0
  27. package/assets/manage/30-status.sh +64 -0
  28. package/assets/manage/40-exit-withdraw.sh +133 -0
  29. package/assets/manage/bond_inc.sh +318 -0
  30. package/assets/manage/dashboard_monitor/app.js +1544 -0
  31. package/assets/manage/dashboard_monitor/assets/v2.2/css/base.css +113 -0
  32. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/content.css +133 -0
  33. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/controls.css +229 -0
  34. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/data.css +374 -0
  35. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/icons.css +35 -0
  36. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/motion.css +57 -0
  37. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/shell.css +51 -0
  38. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/sidebar.css +242 -0
  39. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/topbar.css +138 -0
  40. package/assets/manage/dashboard_monitor/assets/v2.2/css/fonts.css +30 -0
  41. package/assets/manage/dashboard_monitor/assets/v2.2/css/synteq-ui-v2.css +25 -0
  42. package/assets/manage/dashboard_monitor/assets/v2.2/css/theme-dark.css +38 -0
  43. package/assets/manage/dashboard_monitor/assets/v2.2/css/theme-light.css +36 -0
  44. package/assets/manage/dashboard_monitor/assets/v2.2/css/tokens.css +61 -0
  45. package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-Medium.ttf +0 -0
  46. package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-Regular.ttf +0 -0
  47. package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-SemiBold.ttf +0 -0
  48. package/assets/manage/dashboard_monitor/assets/v2.2/js/synteq-ui-v2.js +869 -0
  49. package/assets/manage/dashboard_monitor/deploy-dashboard.sh +230 -0
  50. package/assets/manage/dashboard_monitor/favicon.svg +13 -0
  51. package/assets/manage/dashboard_monitor/index.html +1197 -0
  52. package/assets/manage/dashboard_monitor/nginx-octra.conf +76 -0
  53. package/assets/manage/dashboard_monitor/octra-dashboard.service +29 -0
  54. package/assets/manage/dashboard_monitor/octra-node-prune.cron +12 -0
  55. package/assets/manage/dashboard_monitor/octra-node-prune.service +16 -0
  56. package/assets/manage/dashboard_monitor/octra-node-prune.timer +12 -0
  57. package/assets/manage/dashboard_monitor/prune-node-junk.sh +329 -0
  58. package/assets/manage/dashboard_monitor/server.py +1887 -0
  59. package/assets/manage/dashboard_monitor/ssl-certbot.sh +90 -0
  60. package/assets/manage/dashboard_monitor/styles.css +327 -0
  61. package/assets/manage/lib.sh +347 -0
  62. package/assets/manifest.json +143 -0
  63. package/assets/migrate/00-backup-identity.sh +67 -0
  64. package/assets/migrate/01-prepare-target.sh +145 -0
  65. package/assets/migrate/02-authorize-pull.sh +43 -0
  66. package/assets/migrate/03-pull-state.sh +102 -0
  67. package/assets/migrate/04-stop-source.sh +85 -0
  68. package/assets/migrate/05-finalize-target.sh +140 -0
  69. package/assets/migrate/06-start-target.sh +57 -0
  70. package/assets/migrate/07-verify.sh +237 -0
  71. package/assets/migrate/08-rollback.sh +54 -0
  72. package/assets/migrate/lib.sh +149 -0
  73. package/assets/upgrade/10-preflight.sh +120 -0
  74. package/assets/upgrade/20-upgrade.sh +142 -0
  75. package/assets/upgrade/30-manual-upgrade.sh +129 -0
  76. package/assets/upgrade/40-storage-prune.sh +65 -0
  77. package/assets/upgrade/50-status.sh +58 -0
  78. package/assets/upgrade/lib.sh +232 -0
  79. package/bin/ovtools.js +32 -0
  80. package/dist/api/checks.d.ts +6 -0
  81. package/dist/api/checks.d.ts.map +1 -0
  82. package/dist/api/checks.js +173 -0
  83. package/dist/api/checks.js.map +1 -0
  84. package/dist/api/hostkeys.d.ts +10 -0
  85. package/dist/api/hostkeys.d.ts.map +1 -0
  86. package/dist/api/hostkeys.js +58 -0
  87. package/dist/api/hostkeys.js.map +1 -0
  88. package/dist/api/jobs.d.ts +33 -0
  89. package/dist/api/jobs.d.ts.map +1 -0
  90. package/dist/api/jobs.js +486 -0
  91. package/dist/api/jobs.js.map +1 -0
  92. package/dist/api/nodes.d.ts +16 -0
  93. package/dist/api/nodes.d.ts.map +1 -0
  94. package/dist/api/nodes.js +126 -0
  95. package/dist/api/nodes.js.map +1 -0
  96. package/dist/api/server.d.ts +3 -0
  97. package/dist/api/server.d.ts.map +1 -0
  98. package/dist/api/server.js +443 -0
  99. package/dist/api/server.js.map +1 -0
  100. package/dist/cli.d.ts +3 -0
  101. package/dist/cli.d.ts.map +1 -0
  102. package/dist/cli.js +301 -0
  103. package/dist/cli.js.map +1 -0
  104. package/dist/constants.d.ts +76 -0
  105. package/dist/constants.d.ts.map +1 -0
  106. package/dist/constants.js +150 -0
  107. package/dist/constants.js.map +1 -0
  108. package/dist/index.d.ts +18 -0
  109. package/dist/index.d.ts.map +1 -0
  110. package/dist/index.js +60 -0
  111. package/dist/index.js.map +1 -0
  112. package/dist/orchestrator/base.d.ts +52 -0
  113. package/dist/orchestrator/base.d.ts.map +1 -0
  114. package/dist/orchestrator/base.js +219 -0
  115. package/dist/orchestrator/base.js.map +1 -0
  116. package/dist/orchestrator/deploy.d.ts +33 -0
  117. package/dist/orchestrator/deploy.d.ts.map +1 -0
  118. package/dist/orchestrator/deploy.js +313 -0
  119. package/dist/orchestrator/deploy.js.map +1 -0
  120. package/dist/orchestrator/manage.d.ts +28 -0
  121. package/dist/orchestrator/manage.d.ts.map +1 -0
  122. package/dist/orchestrator/manage.js +267 -0
  123. package/dist/orchestrator/manage.js.map +1 -0
  124. package/dist/orchestrator/migrate.d.ts +31 -0
  125. package/dist/orchestrator/migrate.d.ts.map +1 -0
  126. package/dist/orchestrator/migrate.js +442 -0
  127. package/dist/orchestrator/migrate.js.map +1 -0
  128. package/dist/ssh/client.d.ts +39 -0
  129. package/dist/ssh/client.d.ts.map +1 -0
  130. package/dist/ssh/client.js +418 -0
  131. package/dist/ssh/client.js.map +1 -0
  132. package/dist/test/core.test.d.ts +2 -0
  133. package/dist/test/core.test.d.ts.map +1 -0
  134. package/dist/test/core.test.js +188 -0
  135. package/dist/test/core.test.js.map +1 -0
  136. package/dist/types.d.ts +267 -0
  137. package/dist/types.d.ts.map +1 -0
  138. package/dist/types.js +3 -0
  139. package/dist/types.js.map +1 -0
  140. package/dist/utils/assets.d.ts +28 -0
  141. package/dist/utils/assets.d.ts.map +1 -0
  142. package/dist/utils/assets.js +583 -0
  143. package/dist/utils/assets.js.map +1 -0
  144. package/dist/utils/browser.d.ts +2 -0
  145. package/dist/utils/browser.d.ts.map +1 -0
  146. package/dist/utils/browser.js +23 -0
  147. package/dist/utils/browser.js.map +1 -0
  148. package/dist/utils/dashboard.d.ts +16 -0
  149. package/dist/utils/dashboard.d.ts.map +1 -0
  150. package/dist/utils/dashboard.js +177 -0
  151. package/dist/utils/dashboard.js.map +1 -0
  152. package/dist/utils/helpers.d.ts +29 -0
  153. package/dist/utils/helpers.d.ts.map +1 -0
  154. package/dist/utils/helpers.js +204 -0
  155. package/dist/utils/helpers.js.map +1 -0
  156. package/dist/utils/scripts.d.ts +60 -0
  157. package/dist/utils/scripts.d.ts.map +1 -0
  158. package/dist/utils/scripts.js +174 -0
  159. package/dist/utils/scripts.js.map +1 -0
  160. package/dist/utils/state.d.ts +12 -0
  161. package/dist/utils/state.d.ts.map +1 -0
  162. package/dist/utils/state.js +44 -0
  163. package/dist/utils/state.js.map +1 -0
  164. package/dist/utils/tar.d.ts +11 -0
  165. package/dist/utils/tar.d.ts.map +1 -0
  166. package/dist/utils/tar.js +93 -0
  167. package/dist/utils/tar.js.map +1 -0
  168. package/dist/utils/update.d.ts +9 -0
  169. package/dist/utils/update.d.ts.map +1 -0
  170. package/dist/utils/update.js +134 -0
  171. package/dist/utils/update.js.map +1 -0
  172. package/dist/utils/websocket.d.ts +10 -0
  173. package/dist/utils/websocket.d.ts.map +1 -0
  174. package/dist/utils/websocket.js +203 -0
  175. package/dist/utils/websocket.js.map +1 -0
  176. package/package.json +55 -4
  177. package/public/app.js +2736 -0
  178. package/public/assets/v2.2/css/base.css +113 -0
  179. package/public/assets/v2.2/css/components/content.css +133 -0
  180. package/public/assets/v2.2/css/components/controls.css +229 -0
  181. package/public/assets/v2.2/css/components/data.css +373 -0
  182. package/public/assets/v2.2/css/components/icons.css +35 -0
  183. package/public/assets/v2.2/css/components/motion.css +57 -0
  184. package/public/assets/v2.2/css/components/shell.css +51 -0
  185. package/public/assets/v2.2/css/components/sidebar.css +242 -0
  186. package/public/assets/v2.2/css/components/topbar.css +138 -0
  187. package/public/assets/v2.2/css/fonts.css +30 -0
  188. package/public/assets/v2.2/css/synteq-ui-v2.css +25 -0
  189. package/public/assets/v2.2/css/theme-dark.css +38 -0
  190. package/public/assets/v2.2/css/theme-light.css +36 -0
  191. package/public/assets/v2.2/css/tokens.css +61 -0
  192. package/public/assets/v2.2/fonts/GoogleSansCode-Medium.ttf +0 -0
  193. package/public/assets/v2.2/fonts/GoogleSansCode-Regular.ttf +0 -0
  194. package/public/assets/v2.2/fonts/GoogleSansCode-SemiBold.ttf +0 -0
  195. package/public/assets/v2.2/js/synteq-ui-v2.js +869 -0
  196. package/public/favicon.svg +13 -0
  197. package/public/index.html +1083 -0
  198. package/public/styles.css +580 -0
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env bash
2
+ # ===================================================================
3
+ # ssl-certbot.sh - Let's Encrypt certificate for the live monitor
4
+ #
5
+ # The nginx site always reads /etc/ssl/octra/{fullchain,privkey}.pem, so the
6
+ # Let's Encrypt files are linked there: certbot's own timer renews them and nginx
7
+ # keeps working without any config rewrite.
8
+ #
9
+ # Usage: bash ssl-certbot.sh --domain node-x.example.com [--email you@x] [--min-days 30]
10
+ # Output: SSL_CERTBOT_OK <domain> (issued|reused) / SSL_CERTBOT_ERROR <why>
11
+ # Idempotent: a certificate that is still valid for --min-days is reused.
12
+ # ===================================================================
13
+ set -euo pipefail
14
+
15
+ DOMAIN=""; EMAIL=""; MIN_DAYS=30
16
+ while [[ $# -gt 0 ]]; do
17
+ case "$1" in
18
+ --domain) DOMAIN="${2:-}"; shift 2 ;;
19
+ --email) EMAIL="${2:-}"; shift 2 ;;
20
+ --min-days) MIN_DAYS="${2:-30}"; shift 2 ;;
21
+ *) shift ;;
22
+ esac
23
+ done
24
+
25
+ fail() { echo "SSL_CERTBOT_ERROR $*"; exit 1; }
26
+
27
+ [[ -n "$DOMAIN" ]] || fail "no --domain given"
28
+ [[ $EUID -eq 0 ]] || fail "run as root"
29
+
30
+ LIVE="/etc/ssl/octra"
31
+ LE_LIVE="/etc/letsencrypt/live/${DOMAIN}"
32
+
33
+ # --- certbot present? check -> install -> continue -------------------
34
+ if ! command -v certbot >/dev/null 2>&1; then
35
+ echo "installing certbot (apt)..."
36
+ export DEBIAN_FRONTEND=noninteractive
37
+ apt-get update -qq || fail "apt-get update failed"
38
+ apt-get install -y -qq certbot >/dev/null || fail "could not install certbot"
39
+ fi
40
+
41
+ days_left() { # days_left <pem>
42
+ local file="$1" end now
43
+ [[ -f "$file" ]] || { echo 0; return 0; }
44
+ command -v openssl >/dev/null 2>&1 || { echo 0; return 0; }
45
+ end=$(openssl x509 -enddate -noout -in "$file" 2>/dev/null | cut -d= -f2- || true)
46
+ [[ -n "$end" ]] || { echo 0; return 0; }
47
+ end=$(date -d "$end" +%s 2>/dev/null || echo 0)
48
+ now=$(date +%s)
49
+ [[ "$end" -gt "$now" ]] && echo $(( (end - now) / 86400 )) || echo 0
50
+ }
51
+
52
+ link_into_place() {
53
+ install -d -m 700 "$LIVE"
54
+ ln -sfn "${LE_LIVE}/fullchain.pem" "${LIVE}/fullchain.pem"
55
+ ln -sfn "${LE_LIVE}/privkey.pem" "${LIVE}/privkey.pem"
56
+ chmod 600 "${LE_LIVE}/privkey.pem" 2>/dev/null || true
57
+ }
58
+
59
+ # --- reuse a healthy certificate -------------------------------------
60
+ REUSE=0
61
+ if [[ -f "${LE_LIVE}/fullchain.pem" ]]; then
62
+ left=$(days_left "${LE_LIVE}/fullchain.pem")
63
+ if (( left >= MIN_DAYS )); then REUSE=1; fi
64
+ fi
65
+ if (( REUSE == 0 )); then
66
+ # The standalone challenge needs port 80: stop nginx for the request and bring
67
+ # it back even when certbot fails (a dead nginx would take the monitor down).
68
+ was_active=0
69
+ if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet nginx; then
70
+ was_active=1
71
+ systemctl stop nginx
72
+ fi
73
+ set +e
74
+ if [[ -n "$EMAIL" ]]; then
75
+ certbot certonly --standalone --non-interactive --agree-tos --expand \
76
+ --keep-until-expiring -d "$DOMAIN" -m "$EMAIL"
77
+ rc=$?
78
+ else
79
+ certbot certonly --standalone --non-interactive --agree-tos --expand \
80
+ --keep-until-expiring -d "$DOMAIN" --register-unsafely-without-email
81
+ rc=$?
82
+ fi
83
+ set -e
84
+ if (( was_active == 1 )); then systemctl start nginx || true; fi
85
+ (( rc == 0 )) || fail "certbot exit ${rc} - is DNS for ${DOMAIN} pointing at this host, and is port 80 open?"
86
+ fi
87
+
88
+ [[ -f "${LE_LIVE}/fullchain.pem" ]] || fail "certbot reported success but ${LE_LIVE}/fullchain.pem is missing"
89
+ link_into_place
90
+ echo "SSL_CERTBOT_OK ${DOMAIN} $([[ $REUSE -eq 1 ]] && echo reused || echo issued)"
@@ -0,0 +1,327 @@
1
+ /* ==========================================================================
2
+ OCTRA LIVE MONITOR - dashboard layer
3
+ Built on Synteq UI V2. Everything visual that the framework already ships
4
+ (stat strips, gauges, sparklines, bars, data table, log viewer, status dots,
5
+ copy feedback, mobile breakpoints) comes from the framework; this file only
6
+ holds what is specific to this dashboard.
7
+
8
+ Framework rules kept:
9
+ | single flat canvas, hairline separators, no card backgrounds
10
+ | only .v2-content scrolls, body stays viewport-locked
11
+ | every value uses tokens (no hard-coded colors / spacing)
12
+ ========================================================================== */
13
+
14
+ /* Content rhythm - denser than the framework default for a monitoring view */
15
+ .v2-content {
16
+ gap: var(--v2-space-6);
17
+ padding-top: var(--v2-space-4);
18
+ padding-bottom: var(--v2-space-4);
19
+ }
20
+
21
+ .v2-section--compact {
22
+ gap: var(--v2-space-4);
23
+ padding-block: var(--v2-space-5);
24
+ }
25
+
26
+ /* Panels are grouping primitives only - never surfaces */
27
+ .panel {
28
+ display: flex;
29
+ flex-direction: column;
30
+ gap: var(--v2-space-3);
31
+ min-width: 0;
32
+ }
33
+
34
+ .content-pad { height: var(--v2-space-6); }
35
+
36
+ /* -- Hero ----------------------------------------------------------- */
37
+ .dash-hero { display: flex; flex-direction: column; gap: var(--v2-space-2); min-width: 0; }
38
+ .dash-hero__main { display: flex; flex-direction: column; gap: var(--v2-space-2); min-width: 0; }
39
+ .dash-hero__title-row {
40
+ display: flex;
41
+ align-items: center;
42
+ flex-wrap: wrap;
43
+ gap: var(--v2-space-2) var(--v2-space-3);
44
+ min-width: 0;
45
+ }
46
+ .dash-hero__hostname {
47
+ font-size: var(--v2-text-2xl);
48
+ font-weight: var(--v2-fw-semibold);
49
+ color: var(--v2-text-strong);
50
+ letter-spacing: -0.025em;
51
+ line-height: 1.15;
52
+ overflow-wrap: anywhere;
53
+ }
54
+ .dash-hero__meta {
55
+ display: flex;
56
+ align-items: center;
57
+ flex-wrap: wrap;
58
+ gap: var(--v2-space-2);
59
+ font-size: var(--v2-text-xs);
60
+ color: var(--v2-text-subtle);
61
+ min-width: 0;
62
+ }
63
+ .dash-hero__sep { opacity: 0.6; }
64
+
65
+ /* Copyable validator address */
66
+ .addr-chip {
67
+ display: inline-flex;
68
+ align-items: center;
69
+ gap: var(--v2-space-2);
70
+ max-width: 100%;
71
+ min-width: 0;
72
+ height: 26px;
73
+ padding: 0 var(--v2-space-2) 0 var(--v2-space-3);
74
+ background: var(--v2-inset);
75
+ border-radius: var(--v2-radius-control);
76
+ color: var(--v2-text);
77
+ font-size: var(--v2-text-xs);
78
+ }
79
+ .addr-chip:hover { background: var(--v2-hover); }
80
+ .addr-chip__label {
81
+ display: inline-flex;
82
+ align-items: center;
83
+ gap: var(--v2-space-1);
84
+ color: var(--v2-text-subtle);
85
+ flex-shrink: 0;
86
+ }
87
+ .addr-chip__value {
88
+ color: var(--v2-text-strong);
89
+ overflow: hidden;
90
+ text-overflow: ellipsis;
91
+ white-space: nowrap;
92
+ min-width: 0;
93
+ }
94
+ .addr-chip__copy { color: var(--v2-brand); flex-shrink: 0; }
95
+
96
+ /* -- Alerts --------------------------------------------------------- */
97
+ .alerts { display: flex; flex-direction: column; gap: 3px; }
98
+ .alerts[hidden] { display: none; }
99
+ .alert {
100
+ display: flex;
101
+ align-items: flex-start;
102
+ gap: var(--v2-space-2);
103
+ padding: var(--v2-space-2) var(--v2-space-3);
104
+ font-size: var(--v2-text-sm);
105
+ color: var(--v2-text);
106
+ border-left: 2px solid var(--v2-status-warning);
107
+ background: color-mix(in srgb, var(--v2-status-warning) 8%, transparent);
108
+ }
109
+ .alert--error {
110
+ border-left-color: var(--v2-status-error);
111
+ background: color-mix(in srgb, var(--v2-status-error) 10%, transparent);
112
+ }
113
+ .alert--info {
114
+ border-left-color: var(--v2-brand);
115
+ background: color-mix(in srgb, var(--v2-brand) 8%, transparent);
116
+ }
117
+ .alert__text { min-width: 0; }
118
+ .alert__text strong { color: var(--v2-text-strong); font-weight: var(--v2-fw-medium); }
119
+ /* The row can be dismissed: the x sits at the right end and stays quiet until
120
+ the operator hovers it or tabs to it. */
121
+ .alert__close {
122
+ margin-left: auto;
123
+ display: inline-flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ flex: 0 0 auto;
127
+ width: 20px;
128
+ height: 20px;
129
+ padding: 0;
130
+ border: 0;
131
+ border-radius: var(--v2-radius-sm, 4px);
132
+ background: transparent;
133
+ color: var(--v2-text-mute);
134
+ cursor: pointer;
135
+ opacity: 0.65;
136
+ }
137
+ .alert__close:hover,
138
+ .alert__close:focus-visible {
139
+ opacity: 1;
140
+ color: var(--v2-text-strong);
141
+ background: color-mix(in srgb, currentColor 12%, transparent);
142
+ }
143
+ .alert__close:focus-visible { outline: 2px solid var(--v2-brand); outline-offset: 1px; }
144
+ /* "N message(s) hidden - show them": the rows never disappear silently. */
145
+ .alerts__restore {
146
+ align-self: flex-start;
147
+ padding: 2px var(--v2-space-3);
148
+ font-size: var(--v2-text-xs);
149
+ color: var(--v2-text-mute);
150
+ text-decoration: underline;
151
+ cursor: pointer;
152
+ }
153
+ .alerts__restore:hover { color: var(--v2-brand); }
154
+
155
+ /* -- Hero status: dot + coloured text ------------------------------- */
156
+ .dash-status {
157
+ gap: 6px;
158
+ background: transparent;
159
+ border: 0;
160
+ padding: 0;
161
+ color: var(--v2-text-mute);
162
+ }
163
+ .dash-status--ok { color: var(--v2-status-ok, var(--v2-brand)); }
164
+ .dash-status--warn { color: var(--v2-status-warning); }
165
+ .dash-status--err { color: var(--v2-status-error); }
166
+ .dash-status--info { color: var(--v2-text-mute); }
167
+
168
+ /* -- Health checklist ----------------------------------------------- */
169
+ .health { display: flex; flex-direction: column; }
170
+ .health__row {
171
+ display: flex;
172
+ align-items: baseline;
173
+ gap: var(--v2-space-3);
174
+ padding: var(--v2-space-2) 0;
175
+ border-top: var(--v2-line-h) solid var(--v2-line);
176
+ min-width: 0;
177
+ }
178
+ .health__row:first-child { border-top: none; }
179
+ .health__row .v2-badge { flex-shrink: 0; min-width: 52px; justify-content: center; }
180
+ .health__label { color: var(--v2-text-strong); font-size: var(--v2-text-sm); flex-shrink: 0; }
181
+ .health__detail {
182
+ margin-left: auto;
183
+ font-size: var(--v2-text-xs);
184
+ color: var(--v2-text-subtle);
185
+ text-align: right;
186
+ min-width: 0;
187
+ overflow: hidden;
188
+ text-overflow: ellipsis;
189
+ white-space: nowrap;
190
+ }
191
+
192
+ /* -- Gauges --------------------------------------------------------- */
193
+ .gauge-stack { display: flex; flex-direction: column; gap: var(--v2-space-4); }
194
+
195
+ /* Lag / drift cell emphasis inside .v2-table */
196
+ .v2-table .lag-warn { color: var(--v2-status-warning); }
197
+ .v2-table .lag-err { color: var(--v2-status-error); }
198
+
199
+ /* -- Tables --------------------------------------------------------- */
200
+ /* `.v2-table th` (0,1,1) outranks `.v2-table__num` (0,1,0) in the framework,
201
+ so numeric headers kept `text-align: left` while their cells were right
202
+ aligned - every such header looked shifted into the left neighbour column.
203
+ Mirror the cell alignment on the header and compensate the 0.05em tracking
204
+ that the framework adds after the final glyph. */
205
+ .v2-table th.v2-table__num {
206
+ text-align: right;
207
+ padding-right: calc(var(--v2-space-3) - 0.05em);
208
+ }
209
+ .v2-table th.v2-table__mono { font-family: var(--v2-font-mono); }
210
+
211
+ /* Narrow screens: a wide table scrolls inside .v2-table-wrap (the document
212
+ itself never overflows - verified down to 320 px). Pin the identifying first
213
+ column so a horizontally scrolled row keeps its peer / epoch visible. */
214
+ @media (max-width: 720px) {
215
+ .v2-table-wrap { position: relative; }
216
+ .v2-table th:first-child,
217
+ .v2-table td:first-child {
218
+ position: sticky;
219
+ left: 0;
220
+ z-index: 1;
221
+ background: var(--v2-canvas);
222
+ box-shadow: 1px 0 0 var(--v2-line);
223
+ }
224
+ .v2-table tbody tr:hover td:first-child { background: var(--v2-hover); }
225
+ }
226
+
227
+ /* Wallet KPIs print full-precision token amounts - one notch smaller keeps the
228
+ amount + unit on a single line without overflowing the stat box (the unit is
229
+ glued with a non-breaking space in app.js). */
230
+ #wallets .v2-stat__value { font-size: var(--v2-text-lg); }
231
+
232
+ /* -- Brand mark ----------------------------------------------------- */
233
+ /* Octra "circle filled" glyph drawn in the brand green (no tile behind it) */
234
+ .v2-aside__brand-mark {
235
+ background: transparent;
236
+ color: var(--v2-brand);
237
+ border-radius: 0;
238
+ }
239
+
240
+ /* -- Log toolbar (the viewer itself is a framework component) ------- */
241
+ .log-toolbar {
242
+ display: flex;
243
+ align-items: center;
244
+ flex-wrap: wrap;
245
+ gap: var(--v2-space-2);
246
+ min-width: 0;
247
+ }
248
+ .log-toolbar .v2-seg { flex-shrink: 0; }
249
+ .log-search {
250
+ flex: 1 1 220px;
251
+ min-width: 140px;
252
+ height: 32px;
253
+ }
254
+ .log-meta { margin-left: auto; white-space: nowrap; }
255
+
256
+ #log-body { --v2-log-h: 420px; }
257
+
258
+ /* -- Utility bits --------------------------------------------------- */
259
+ .truncate {
260
+ display: inline-block;
261
+ max-width: 100%;
262
+ overflow: hidden;
263
+ text-overflow: ellipsis;
264
+ white-space: nowrap;
265
+ vertical-align: bottom;
266
+ }
267
+ .hash-cell {
268
+ display: inline-flex;
269
+ align-items: center;
270
+ gap: var(--v2-space-1);
271
+ min-width: 0;
272
+ max-width: 62%;
273
+ }
274
+ .copy-btn { width: 24px; height: 24px; flex-shrink: 0; }
275
+ .sys-foot { margin-top: var(--v2-space-1); }
276
+
277
+ /* Topbar live readout */
278
+ .live-age { color: var(--v2-text-subtle); white-space: nowrap; }
279
+ .live-age.is-stale { color: var(--v2-status-warning); }
280
+
281
+ /* Bottom navigation badge: the validator status (was the chain id).
282
+ It keeps the badge chrome - a tinted pill in coloured tone - unlike the hero
283
+ status, which is dot + text without a background. */
284
+ .v2-aside__footer .dash-status {
285
+ max-width: 100%;
286
+ white-space: nowrap;
287
+ overflow: hidden;
288
+ font-size: var(--v2-text-xs);
289
+ }
290
+ .v2-aside__footer .v2-badge.dash-status {
291
+ padding: 2px 8px;
292
+ border-radius: 999px;
293
+ border: 1px solid color-mix(in srgb, currentColor 32%, transparent);
294
+ background: color-mix(in srgb, currentColor 14%, transparent);
295
+ }
296
+ .v2-aside__footer .dash-status .v2-dot { flex: 0 0 auto; }
297
+
298
+ /* Refresh button feedback */
299
+ @keyframes dash-spin { to { transform: rotate(360deg); } }
300
+ .is-spinning .v2-icon { animation: dash-spin 0.8s linear; }
301
+
302
+ /* -- Responsive (framework handles grid/table/log breakpoints) ------ */
303
+ @media (max-width: 720px) {
304
+ .v2-content { gap: var(--v2-space-5); padding-inline: var(--v2-space-4); }
305
+ .v2-section--compact { padding-block: var(--v2-space-4); }
306
+ .dash-hero__hostname { font-size: var(--v2-text-xl); }
307
+ .health__detail { display: none; }
308
+ .hash-cell { max-width: 100%; }
309
+ #log-body { --v2-log-h: 360px; }
310
+ }
311
+
312
+ @media (max-width: 600px) {
313
+ .live-age { display: none; }
314
+ .log-meta { display: none; }
315
+ }
316
+
317
+ @media (max-width: 520px) {
318
+ /* The hero already carries the status - keep the topbar uncluttered */
319
+ #topbar-status { display: none; }
320
+ }
321
+
322
+ @media (max-width: 420px) {
323
+ .v2-content { padding-inline: var(--v2-space-3); }
324
+ .addr-chip__label { display: none; }
325
+ .dash-hero__hostname { font-size: var(--v2-text-lg); }
326
+ }
327
+