Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0be4512293 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "amnezia-fellow"
|
name = "amnezia-fellow"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Amnezia VPN client manager with SSO, SQLite, and Kubernetes Secret sync"
|
description = "Amnezia VPN client manager with SSO, SQLite, and Kubernetes Secret sync"
|
||||||
|
|
||||||
|
|||||||
@@ -93,16 +93,23 @@ translations! {
|
|||||||
configs_loading_status: "Loading status..." , "Загрузка статуса...";
|
configs_loading_status: "Loading status..." , "Загрузка статуса...";
|
||||||
configs_no_pods: "No AmneziaWG pods." , "Нет подов AmneziaWG.";
|
configs_no_pods: "No AmneziaWG pods." , "Нет подов AmneziaWG.";
|
||||||
configs_server: "Server" , "Сервер";
|
configs_server: "Server" , "Сервер";
|
||||||
|
configs_endpoint: "Endpoint" , "Endpoint";
|
||||||
|
configs_message: "Message" , "Сообщение";
|
||||||
configs_pod: "Pod" , "Pod";
|
configs_pod: "Pod" , "Pod";
|
||||||
|
configs_node: "Node" , "Нода";
|
||||||
configs_rollout: "Rollout" , "Применение";
|
configs_rollout: "Rollout" , "Применение";
|
||||||
configs_ready: "Ready" , "Готов";
|
configs_ready: "Ready" , "Готов";
|
||||||
configs_not_ready: "Not ready" , "Не готов";
|
configs_not_ready: "Not ready" , "Не готов";
|
||||||
configs_phase: "Phase" , "Фаза";
|
configs_phase: "Phase" , "Фаза";
|
||||||
configs_uptime: "Uptime" , "Аптайм";
|
configs_uptime: "Uptime" , "Аптайм";
|
||||||
configs_restarts: "Restarts" , "Рестарты";
|
configs_restarts: "Restarts" , "Рестарты";
|
||||||
|
configs_config_applied: "Config applied" , "Конфиг применён";
|
||||||
|
configs_reload_status: "Reload status" , "Статус reload";
|
||||||
|
configs_config_hash: "Config hash" , "Хэш конфига";
|
||||||
configs_status_applied: "applied" , "применён";
|
configs_status_applied: "applied" , "применён";
|
||||||
configs_status_starting: "starting" , "стартует";
|
configs_status_starting: "starting" , "стартует";
|
||||||
configs_status_pending_restart: "pending restart" , "ждёт рестарт";
|
configs_status_pending_restart: "pending restart" , "ждёт рестарт";
|
||||||
|
configs_status_error: "reload error" , "ошибка reload";
|
||||||
configs_status_unknown: "unknown" , "неизвестно";
|
configs_status_unknown: "unknown" , "неизвестно";
|
||||||
configs_never: "n/a" , "н/д";
|
configs_never: "n/a" , "н/д";
|
||||||
configs_servers: "Servers" , "Серверы";
|
configs_servers: "Servers" , "Серверы";
|
||||||
|
|||||||
+57
-2
@@ -399,6 +399,12 @@ fn qcompress(data: &[u8]) -> Result<Vec<u8>, String> {
|
|||||||
|
|
||||||
const CLIENT_SECRET_UPDATED_AT_ANNOTATION: &str =
|
const CLIENT_SECRET_UPDATED_AT_ANNOTATION: &str =
|
||||||
"amnezia-fellow.hexor.cy/client-secret-updated-at-ms";
|
"amnezia-fellow.hexor.cy/client-secret-updated-at-ms";
|
||||||
|
const CLIENT_SECRET_APPLIED_AT_ANNOTATION: &str =
|
||||||
|
"amnezia-fellow.hexor.cy/client-secret-applied-at-ms";
|
||||||
|
const CLIENT_SECRET_APPLIED_HASH_ANNOTATION: &str =
|
||||||
|
"amnezia-fellow.hexor.cy/client-secret-applied-hash";
|
||||||
|
const CLIENT_SECRET_RELOAD_STATUS_ANNOTATION: &str =
|
||||||
|
"amnezia-fellow.hexor.cy/client-secret-reload-status";
|
||||||
const AMNEZIAWG_POD_LABEL_SELECTOR: &str = "app=amneziawg";
|
const AMNEZIAWG_POD_LABEL_SELECTOR: &str = "app=amneziawg";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||||
@@ -613,6 +619,9 @@ pub struct VpnPodRolloutStatus {
|
|||||||
pub ready: bool,
|
pub ready: bool,
|
||||||
pub restart_count: i32,
|
pub restart_count: i32,
|
||||||
pub started_at_ms: Option<i64>,
|
pub started_at_ms: Option<i64>,
|
||||||
|
pub config_applied_at_ms: Option<i64>,
|
||||||
|
pub config_applied_hash: Option<String>,
|
||||||
|
pub config_reload_status: Option<String>,
|
||||||
pub uptime_seconds: Option<u64>,
|
pub uptime_seconds: Option<u64>,
|
||||||
pub rollout_status: String,
|
pub rollout_status: String,
|
||||||
pub message: Option<String>,
|
pub message: Option<String>,
|
||||||
@@ -705,6 +714,7 @@ fn pod_rollout_status(
|
|||||||
config_updated_at_ms: Option<i64>,
|
config_updated_at_ms: Option<i64>,
|
||||||
now_ms: i64,
|
now_ms: i64,
|
||||||
) -> VpnPodRolloutStatus {
|
) -> VpnPodRolloutStatus {
|
||||||
|
let annotations = pod.metadata.annotations.clone().unwrap_or_default();
|
||||||
let name = pod.metadata.name.unwrap_or_default();
|
let name = pod.metadata.name.unwrap_or_default();
|
||||||
let node_name = pod
|
let node_name = pod
|
||||||
.spec
|
.spec
|
||||||
@@ -728,11 +738,26 @@ fn pod_rollout_status(
|
|||||||
let started_at_ms = status
|
let started_at_ms = status
|
||||||
.and_then(|status| status.start_time.as_ref())
|
.and_then(|status| status.start_time.as_ref())
|
||||||
.map(time_to_millis);
|
.map(time_to_millis);
|
||||||
|
let config_applied_at_ms = annotation_i64(&annotations, CLIENT_SECRET_APPLIED_AT_ANNOTATION);
|
||||||
|
let config_applied_hash = annotations
|
||||||
|
.get(CLIENT_SECRET_APPLIED_HASH_ANNOTATION)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.cloned();
|
||||||
|
let config_reload_status = annotations
|
||||||
|
.get(CLIENT_SECRET_RELOAD_STATUS_ANNOTATION)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.cloned();
|
||||||
let uptime_seconds = started_at_ms.map(|started| {
|
let uptime_seconds = started_at_ms.map(|started| {
|
||||||
let elapsed_ms = now_ms.saturating_sub(started);
|
let elapsed_ms = now_ms.saturating_sub(started);
|
||||||
(elapsed_ms / 1000) as u64
|
(elapsed_ms / 1000) as u64
|
||||||
});
|
});
|
||||||
let rollout_status = rollout_status(config_updated_at_ms, started_at_ms, ready);
|
let rollout_status = rollout_status(
|
||||||
|
config_updated_at_ms,
|
||||||
|
started_at_ms,
|
||||||
|
config_applied_at_ms,
|
||||||
|
config_reload_status.as_deref(),
|
||||||
|
ready,
|
||||||
|
);
|
||||||
let message = status.and_then(|status| status.message.clone().or(status.reason.clone()));
|
let message = status.and_then(|status| status.message.clone().or(status.reason.clone()));
|
||||||
|
|
||||||
VpnPodRolloutStatus {
|
VpnPodRolloutStatus {
|
||||||
@@ -744,6 +769,9 @@ fn pod_rollout_status(
|
|||||||
ready,
|
ready,
|
||||||
restart_count,
|
restart_count,
|
||||||
started_at_ms,
|
started_at_ms,
|
||||||
|
config_applied_at_ms,
|
||||||
|
config_applied_hash,
|
||||||
|
config_reload_status,
|
||||||
uptime_seconds,
|
uptime_seconds,
|
||||||
rollout_status,
|
rollout_status,
|
||||||
message,
|
message,
|
||||||
@@ -781,9 +809,16 @@ fn amneziawg_restart_count(status: &k8s_openapi::api::core::v1::PodStatus) -> i3
|
|||||||
fn rollout_status(
|
fn rollout_status(
|
||||||
config_updated_at_ms: Option<i64>,
|
config_updated_at_ms: Option<i64>,
|
||||||
started_at_ms: Option<i64>,
|
started_at_ms: Option<i64>,
|
||||||
|
config_applied_at_ms: Option<i64>,
|
||||||
|
config_reload_status: Option<&str>,
|
||||||
ready: bool,
|
ready: bool,
|
||||||
) -> String {
|
) -> String {
|
||||||
match (config_updated_at_ms, started_at_ms) {
|
if config_reload_status == Some("error") {
|
||||||
|
return "error".to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
let effective_applied_at_ms = started_at_ms.into_iter().chain(config_applied_at_ms).max();
|
||||||
|
match (config_updated_at_ms, effective_applied_at_ms) {
|
||||||
(None, _) => "unknown".to_owned(),
|
(None, _) => "unknown".to_owned(),
|
||||||
(Some(_), None) => "pending_restart".to_owned(),
|
(Some(_), None) => "pending_restart".to_owned(),
|
||||||
(Some(updated), Some(started)) if started >= updated && ready => "applied".to_owned(),
|
(Some(updated), Some(started)) if started >= updated && ready => "applied".to_owned(),
|
||||||
@@ -802,6 +837,10 @@ fn client_secret_updated_at_ms(secret: &Secret) -> Option<i64> {
|
|||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn annotation_i64(annotations: &BTreeMap<String, String>, key: &str) -> Option<i64> {
|
||||||
|
annotations.get(key)?.parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
fn time_to_millis(time: &Time) -> i64 {
|
fn time_to_millis(time: &Time) -> i64 {
|
||||||
time.0.as_millisecond()
|
time.0.as_millisecond()
|
||||||
}
|
}
|
||||||
@@ -1037,6 +1076,22 @@ mod tests {
|
|||||||
assert!(!overrides.contains_key("empty"));
|
assert!(!overrides.contains_key("empty"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rollout_status_uses_hot_reload_annotations() {
|
||||||
|
assert_eq!(
|
||||||
|
rollout_status(Some(2_000), Some(1_000), Some(3_000), Some("applied"), true),
|
||||||
|
"applied"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rollout_status(Some(2_000), Some(1_000), None, Some("error"), true),
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rollout_status(Some(2_000), Some(1_000), None, None, true),
|
||||||
|
"pending_restart"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn qcompress_uses_qt_wire_format() {
|
fn qcompress_uses_qt_wire_format() {
|
||||||
let payload = br#"{"description":"de-fsn1"}"#;
|
let payload = br#"{"description":"de-fsn1"}"#;
|
||||||
|
|||||||
+68
-5
@@ -45,24 +45,28 @@
|
|||||||
.rollout { margin: 1rem 0 1.25rem; }
|
.rollout { margin: 1rem 0 1.25rem; }
|
||||||
.rollout-meta { color: #53606d; font-size: .88rem; margin-bottom: .5rem; }
|
.rollout-meta { color: #53606d; font-size: .88rem; margin-bottom: .5rem; }
|
||||||
.rollout-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: .55rem; }
|
.rollout-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: .55rem; }
|
||||||
.rollout-tile { position: relative; min-height: 86px; border: 1px solid #dde2e6; border-left-width: 4px; border-radius: 6px; background: #fff; padding: .65rem .7rem; display: grid; gap: .45rem; align-content: space-between; }
|
.rollout-tile { position: relative; min-height: 86px; border: 1px solid #dde2e6; border-left-width: 4px; border-radius: 6px; background: #fff; padding: .65rem .7rem; display: grid; gap: .45rem; align-content: space-between; cursor: pointer; }
|
||||||
|
.rollout-tile:focus { outline: 2px solid #7aa7c7; outline-offset: 2px; }
|
||||||
.rollout-tile::after { content: attr(data-tooltip); position: absolute; left: .25rem; top: calc(100% + .45rem); z-index: 5; width: max-content; max-width: min(380px, 80vw); padding: .55rem .65rem; border-radius: 4px; background: #17202a; color: #fff; font-size: .78rem; line-height: 1.35; white-space: pre-line; box-shadow: 0 8px 24px rgba(23, 32, 42, .22); opacity: 0; pointer-events: none; transform: translateY(-3px); transition: opacity .12s ease, transform .12s ease; }
|
.rollout-tile::after { content: attr(data-tooltip); position: absolute; left: .25rem; top: calc(100% + .45rem); z-index: 5; width: max-content; max-width: min(380px, 80vw); padding: .55rem .65rem; border-radius: 4px; background: #17202a; color: #fff; font-size: .78rem; line-height: 1.35; white-space: pre-line; box-shadow: 0 8px 24px rgba(23, 32, 42, .22); opacity: 0; pointer-events: none; transform: translateY(-3px); transition: opacity .12s ease, transform .12s ease; }
|
||||||
.rollout-tile:hover::after, .rollout-tile:focus-within::after { opacity: 1; transform: translateY(0); }
|
.rollout-tile:hover::after, .rollout-tile:focus-within::after { opacity: 1; transform: translateY(0); }
|
||||||
.rollout-tile-applied { border-left-color: #2f8f4e; }
|
.rollout-tile-applied { border-left-color: #2f8f4e; }
|
||||||
.rollout-tile-starting { border-left-color: #d39a00; }
|
.rollout-tile-starting { border-left-color: #d39a00; }
|
||||||
.rollout-tile-pending_restart { border-left-color: #c83f31; }
|
.rollout-tile-pending_restart { border-left-color: #c83f31; }
|
||||||
|
.rollout-tile-error { border-left-color: #c83f31; }
|
||||||
.rollout-tile-unknown { border-left-color: #7b8793; }
|
.rollout-tile-unknown { border-left-color: #7b8793; }
|
||||||
.rollout-tile-head { display: flex; align-items: center; min-width: 0; gap: .45rem; }
|
.rollout-tile-head { display: flex; align-items: center; min-width: 0; gap: .45rem; }
|
||||||
.rollout-dot { width: .55rem; height: .55rem; border-radius: 999px; flex: 0 0 auto; background: #7b8793; }
|
.rollout-dot { width: .55rem; height: .55rem; border-radius: 999px; flex: 0 0 auto; background: #7b8793; }
|
||||||
.rollout-dot-applied { background: #2f8f4e; }
|
.rollout-dot-applied { background: #2f8f4e; }
|
||||||
.rollout-dot-starting { background: #d39a00; }
|
.rollout-dot-starting { background: #d39a00; }
|
||||||
.rollout-dot-pending_restart { background: #c83f31; }
|
.rollout-dot-pending_restart { background: #c83f31; }
|
||||||
|
.rollout-dot-error { background: #c83f31; }
|
||||||
.rollout-dot-unknown { background: #7b8793; }
|
.rollout-dot-unknown { background: #7b8793; }
|
||||||
.rollout-server { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 700; color: #1d252d; }
|
.rollout-server { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 700; color: #1d252d; }
|
||||||
.rollout-badge { display: inline-flex; align-items: center; min-height: 24px; padding: .15rem .45rem; border-radius: 4px; font-size: .82rem; font-weight: 700; white-space: nowrap; }
|
.rollout-badge { display: inline-flex; align-items: center; min-height: 24px; padding: .15rem .45rem; border-radius: 4px; font-size: .82rem; font-weight: 700; white-space: nowrap; }
|
||||||
.rollout-applied { background: #dcefe4; color: #175c31; }
|
.rollout-applied { background: #dcefe4; color: #175c31; }
|
||||||
.rollout-starting { background: #fff0c2; color: #6f4e00; }
|
.rollout-starting { background: #fff0c2; color: #6f4e00; }
|
||||||
.rollout-pending_restart { background: #ffe3df; color: #8b2116; }
|
.rollout-pending_restart { background: #ffe3df; color: #8b2116; }
|
||||||
|
.rollout-error { background: #ffe3df; color: #8b2116; }
|
||||||
.rollout-unknown { background: #e8ecef; color: #53606d; }
|
.rollout-unknown { background: #e8ecef; color: #53606d; }
|
||||||
.rollout-foot { display: flex; align-items: center; justify-content: space-between; gap: .5rem; color: #53606d; font-size: .82rem; }
|
.rollout-foot { display: flex; align-items: center; justify-content: space-between; gap: .5rem; color: #53606d; font-size: .82rem; }
|
||||||
.server-name { font-weight: 650; color: #34414f; overflow-wrap: anywhere; }
|
.server-name { font-weight: 650; color: #34414f; overflow-wrap: anywhere; }
|
||||||
@@ -80,6 +84,11 @@
|
|||||||
.server-card { border: 1px solid #dde2e6; border-radius: 6px; padding: .7rem; display: grid; grid-template-columns: minmax(160px, 1fr) auto; gap: .65rem; align-items: center; }
|
.server-card { border: 1px solid #dde2e6; border-radius: 6px; padding: .7rem; display: grid; grid-template-columns: minmax(160px, 1fr) auto; gap: .65rem; align-items: center; }
|
||||||
.server-card-main { display: grid; gap: .25rem; min-width: 0; }
|
.server-card-main { display: grid; gap: .25rem; min-width: 0; }
|
||||||
.server-card-actions { display: flex; gap: .45rem; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
|
.server-card-actions { display: flex; gap: .45rem; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
|
||||||
|
.detail-list { display: grid; gap: .55rem; overflow: auto; }
|
||||||
|
.detail-row { display: grid; grid-template-columns: minmax(120px, .45fr) minmax(0, 1fr); gap: .75rem; align-items: start; padding: .55rem 0; border-bottom: 1px solid #edf0f2; }
|
||||||
|
.detail-row:last-child { border-bottom: 0; }
|
||||||
|
.detail-label { color: #53606d; font-size: .84rem; }
|
||||||
|
.detail-value { color: #1d252d; overflow-wrap: anywhere; }
|
||||||
.qr-box { display: grid; place-items: center; padding: .75rem; border: 1px solid #dde2e6; border-radius: 6px; background: #fff; }
|
.qr-box { display: grid; place-items: center; padding: .75rem; border: 1px solid #dde2e6; border-radius: 6px; background: #fff; }
|
||||||
.qr-box svg { width: min(280px, 100%); height: auto; display: block; }
|
.qr-box svg { width: min(280px, 100%); height: auto; display: block; }
|
||||||
.modal-actions { display: flex; gap: .5rem; justify-content: flex-end; margin-top: .75rem; flex-wrap: wrap; }
|
.modal-actions { display: flex; gap: .5rem; justify-content: flex-end; margin-top: .75rem; flex-wrap: wrap; }
|
||||||
@@ -98,6 +107,7 @@
|
|||||||
.server-card { grid-template-columns: 1fr; }
|
.server-card { grid-template-columns: 1fr; }
|
||||||
.server-card-actions { justify-content: stretch; }
|
.server-card-actions { justify-content: stretch; }
|
||||||
.server-card-actions button { flex: 1 1 100%; }
|
.server-card-actions button { flex: 1 1 100%; }
|
||||||
|
.detail-row { grid-template-columns: 1fr; gap: .2rem; }
|
||||||
.rollout-grid { grid-template-columns: 1fr; }
|
.rollout-grid { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -161,7 +171,7 @@
|
|||||||
<template x-if="rollout && rollout.pods.length > 0">
|
<template x-if="rollout && rollout.pods.length > 0">
|
||||||
<div class="rollout-grid">
|
<div class="rollout-grid">
|
||||||
<template x-for="pod in rollout.pods" :key="pod.name">
|
<template x-for="pod in rollout.pods" :key="pod.name">
|
||||||
<div class="rollout-tile" :class="`rollout-tile-${pod.rollout_status}`" :data-tooltip="podTooltip(pod)" :title="podTooltip(pod)" tabindex="0">
|
<div class="rollout-tile" :class="`rollout-tile-${pod.rollout_status}`" :data-tooltip="podTooltip(pod)" :title="podTooltip(pod)" tabindex="0" role="button" @click="openRollout(pod)" @keydown.enter.prevent="openRollout(pod)" @keydown.space.prevent="openRollout(pod)">
|
||||||
<div class="rollout-tile-head">
|
<div class="rollout-tile-head">
|
||||||
<span class="rollout-dot" :class="`rollout-dot-${pod.rollout_status}`"></span>
|
<span class="rollout-dot" :class="`rollout-dot-${pod.rollout_status}`"></span>
|
||||||
<span class="rollout-server" x-text="pod.endpoint_name || pod.node_name || pod.name"></span>
|
<span class="rollout-server" x-text="pod.endpoint_name || pod.node_name || pod.name"></span>
|
||||||
@@ -230,6 +240,37 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template x-if="rolloutModal">
|
||||||
|
<div class="modal-backdrop" @click.self="closeRollout()">
|
||||||
|
<div class="modal servers-modal">
|
||||||
|
<div class="modal-head">
|
||||||
|
<div class="modal-title">
|
||||||
|
<h3 x-text="podServerName(rolloutModal)"></h3>
|
||||||
|
<div class="modal-subtitle" x-text="rolloutModal.endpoint || rolloutModal.node_name || rolloutModal.name"></div>
|
||||||
|
</div>
|
||||||
|
<button class="secondary" @click="closeRollout()">{{ t.admin_close }}</button>
|
||||||
|
</div>
|
||||||
|
<div class="detail-list">
|
||||||
|
<div class="detail-row"><span class="detail-label">{{ t.configs_rollout }}</span><span class="detail-value"><span class="rollout-badge" :class="`rollout-${rolloutModal.rollout_status}`" x-text="rolloutStatusLabel(rolloutModal.rollout_status)"></span></span></div>
|
||||||
|
<div class="detail-row"><span class="detail-label">{{ t.configs_pod }}</span><span class="detail-value"><code x-text="rolloutModal.name"></code></span></div>
|
||||||
|
<div class="detail-row"><span class="detail-label">{{ t.configs_node }}</span><span class="detail-value"><code x-text="rolloutModal.node_name || '{{ t.configs_never }}'"></code></span></div>
|
||||||
|
<div class="detail-row"><span class="detail-label">{{ t.configs_endpoint }}</span><span class="detail-value"><code x-text="rolloutModal.endpoint || '{{ t.configs_never }}'"></code></span></div>
|
||||||
|
<div class="detail-row"><span class="detail-label">{{ t.configs_ready }}</span><span class="detail-value" x-text="rolloutModal.ready ? '{{ t.configs_yes }}' : '{{ t.configs_no }}'"></span></div>
|
||||||
|
<div class="detail-row"><span class="detail-label">{{ t.configs_phase }}</span><span class="detail-value" x-text="rolloutModal.phase || '{{ t.configs_status_unknown }}'"></span></div>
|
||||||
|
<div class="detail-row"><span class="detail-label">{{ t.configs_uptime }}</span><span class="detail-value" x-text="formatDuration(rolloutModal.uptime_seconds)"></span></div>
|
||||||
|
<div class="detail-row"><span class="detail-label">{{ t.configs_restarts }}</span><span class="detail-value" x-text="rolloutModal.restart_count"></span></div>
|
||||||
|
<div class="detail-row"><span class="detail-label">{{ t.configs_config_updated }}</span><span class="detail-value" x-text="formatTime(rollout && rollout.config_updated_at_ms)"></span></div>
|
||||||
|
<div class="detail-row"><span class="detail-label">{{ t.configs_config_applied }}</span><span class="detail-value" x-text="formatTime(rolloutModal.config_applied_at_ms)"></span></div>
|
||||||
|
<div class="detail-row"><span class="detail-label">{{ t.configs_reload_status }}</span><span class="detail-value" x-text="reloadStatusLabel(rolloutModal.config_reload_status)"></span></div>
|
||||||
|
<div class="detail-row"><span class="detail-label">{{ t.configs_config_hash }}</span><span class="detail-value"><code x-text="shortHash(rolloutModal.config_applied_hash) || '{{ t.configs_never }}'"></code></span></div>
|
||||||
|
<template x-if="rolloutModal.message">
|
||||||
|
<div class="detail-row"><span class="detail-label">{{ t.configs_message }}</span><span class="detail-value" x-text="rolloutModal.message"></span></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template x-if="serverModal">
|
<template x-if="serverModal">
|
||||||
<div class="modal-backdrop" @click.self="closeServers()">
|
<div class="modal-backdrop" @click.self="closeServers()">
|
||||||
<div class="modal servers-modal">
|
<div class="modal servers-modal">
|
||||||
@@ -294,6 +335,7 @@ function configsPage() {
|
|||||||
rolloutBusy: false,
|
rolloutBusy: false,
|
||||||
rolloutError: '',
|
rolloutError: '',
|
||||||
rolloutTimer: null,
|
rolloutTimer: null,
|
||||||
|
rolloutModal: null,
|
||||||
serverConfigs: {},
|
serverConfigs: {},
|
||||||
serverModal: null,
|
serverModal: null,
|
||||||
qrModal: null,
|
qrModal: null,
|
||||||
@@ -514,6 +556,12 @@ function configsPage() {
|
|||||||
this.rolloutBusy = false;
|
this.rolloutBusy = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
openRollout(pod) {
|
||||||
|
this.rolloutModal = pod;
|
||||||
|
},
|
||||||
|
closeRollout() {
|
||||||
|
this.rolloutModal = null;
|
||||||
|
},
|
||||||
clientServers(client) {
|
clientServers(client) {
|
||||||
return (this.serverConfigs[client.id] && this.serverConfigs[client.id].servers) || [];
|
return (this.serverConfigs[client.id] && this.serverConfigs[client.id].servers) || [];
|
||||||
},
|
},
|
||||||
@@ -542,6 +590,11 @@ function configsPage() {
|
|||||||
if (!key || key.length <= 16) return key || '';
|
if (!key || key.length <= 16) return key || '';
|
||||||
return `${key.slice(0, 8)}...${key.slice(-8)}`;
|
return `${key.slice(0, 8)}...${key.slice(-8)}`;
|
||||||
},
|
},
|
||||||
|
shortHash(hash) {
|
||||||
|
if (!hash) return '';
|
||||||
|
if (hash.length <= 16) return hash;
|
||||||
|
return `${hash.slice(0, 12)}...${hash.slice(-8)}`;
|
||||||
|
},
|
||||||
fileSlug(value) {
|
fileSlug(value) {
|
||||||
return String(value || 'amnezia').trim().replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'amnezia';
|
return String(value || 'amnezia').trim().replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'amnezia';
|
||||||
},
|
},
|
||||||
@@ -550,22 +603,32 @@ function configsPage() {
|
|||||||
applied: '{{ t.configs_status_applied }}',
|
applied: '{{ t.configs_status_applied }}',
|
||||||
starting: '{{ t.configs_status_starting }}',
|
starting: '{{ t.configs_status_starting }}',
|
||||||
pending_restart: '{{ t.configs_status_pending_restart }}',
|
pending_restart: '{{ t.configs_status_pending_restart }}',
|
||||||
|
error: '{{ t.configs_status_error }}',
|
||||||
unknown: '{{ t.configs_status_unknown }}',
|
unknown: '{{ t.configs_status_unknown }}',
|
||||||
};
|
};
|
||||||
return labels[value] || value || '{{ t.configs_status_unknown }}';
|
return labels[value] || value || '{{ t.configs_status_unknown }}';
|
||||||
},
|
},
|
||||||
|
reloadStatusLabel(value) {
|
||||||
|
if (!value) return '{{ t.configs_never }}';
|
||||||
|
return this.rolloutStatusLabel(value);
|
||||||
|
},
|
||||||
|
podServerName(pod) {
|
||||||
|
return pod.endpoint_name || pod.node_name || pod.name || '{{ t.configs_status_unknown }}';
|
||||||
|
},
|
||||||
podTooltip(pod) {
|
podTooltip(pod) {
|
||||||
return [
|
return [
|
||||||
`{{ t.configs_server }}: ${pod.endpoint_name || pod.node_name || pod.name}`,
|
`{{ t.configs_server }}: ${this.podServerName(pod)}`,
|
||||||
`{{ t.configs_pod }}: ${pod.name}`,
|
`{{ t.configs_pod }}: ${pod.name}`,
|
||||||
pod.endpoint ? `Endpoint: ${pod.endpoint}` : null,
|
pod.endpoint ? `{{ t.configs_endpoint }}: ${pod.endpoint}` : null,
|
||||||
`{{ t.configs_rollout }}: ${this.rolloutStatusLabel(pod.rollout_status)}`,
|
`{{ t.configs_rollout }}: ${this.rolloutStatusLabel(pod.rollout_status)}`,
|
||||||
`{{ t.configs_ready }}: ${pod.ready ? '{{ t.configs_yes }}' : '{{ t.configs_no }}'}`,
|
`{{ t.configs_ready }}: ${pod.ready ? '{{ t.configs_yes }}' : '{{ t.configs_no }}'}`,
|
||||||
`{{ t.configs_phase }}: ${pod.phase || '{{ t.configs_status_unknown }}'}`,
|
`{{ t.configs_phase }}: ${pod.phase || '{{ t.configs_status_unknown }}'}`,
|
||||||
`{{ t.configs_uptime }}: ${this.formatDuration(pod.uptime_seconds)}`,
|
`{{ t.configs_uptime }}: ${this.formatDuration(pod.uptime_seconds)}`,
|
||||||
`{{ t.configs_restarts }}: ${pod.restart_count}`,
|
`{{ t.configs_restarts }}: ${pod.restart_count}`,
|
||||||
`{{ t.configs_config_updated }}: ${this.formatTime(this.rollout && this.rollout.config_updated_at_ms)}`,
|
`{{ t.configs_config_updated }}: ${this.formatTime(this.rollout && this.rollout.config_updated_at_ms)}`,
|
||||||
pod.message ? `Message: ${pod.message}` : null,
|
`{{ t.configs_config_applied }}: ${this.formatTime(pod.config_applied_at_ms)}`,
|
||||||
|
pod.config_reload_status ? `{{ t.configs_reload_status }}: ${this.reloadStatusLabel(pod.config_reload_status)}` : null,
|
||||||
|
pod.message ? `{{ t.configs_message }}: ${pod.message}` : null,
|
||||||
].filter(Boolean).join('\n');
|
].filter(Boolean).join('\n');
|
||||||
},
|
},
|
||||||
formatTime(ms) {
|
formatTime(ms) {
|
||||||
|
|||||||
Reference in New Issue
Block a user