Add managed Gitea updates and server installer
This commit is contained in:
31
README.md
31
README.md
@@ -44,6 +44,11 @@
|
|||||||
Готовые версии для Linux публикуются в разделе Releases репозитория. Бинарник
|
Готовые версии для Linux публикуются в разделе Releases репозитория. Бинарник
|
||||||
не требует установки Go или системной библиотеки SQLite.
|
не требует установки Go или системной библиотеки SQLite.
|
||||||
|
|
||||||
|
Проверка обновлений выполняется отдельным systemd-таймером раз в сутки. Статус
|
||||||
|
виден во вкладке `Настройки → Обновления` и в центре уведомлений. Доступны два
|
||||||
|
режима: `notify` только сообщает о новой версии, `auto` скачивает её, сохраняет
|
||||||
|
предыдущий бинарник как резервный и перезапускает панель.
|
||||||
|
|
||||||
На главном экране находятся компактные карточки CPU, памяти, диска и сети.
|
На главном экране находятся компактные карточки CPU, памяти, диска и сети.
|
||||||
Нажатие на карточку открывает подробные показатели.
|
Нажатие на карточку открывает подробные показатели.
|
||||||
|
|
||||||
@@ -56,6 +61,32 @@ chmod +x proxmox-cpu-dashboard-linux-amd64
|
|||||||
./proxmox-cpu-dashboard-linux-amd64
|
./proxmox-cpu-dashboard-linux-amd64
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Установка на сервер и обновления
|
||||||
|
|
||||||
|
Скачайте из Releases архив `proxmox-dashboard-install-amd64.tar.gz`, распакуйте
|
||||||
|
его на Proxmox и запустите от root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar -xzf proxmox-dashboard-install-amd64.tar.gz
|
||||||
|
cd proxmox-dashboard-install
|
||||||
|
UPDATE_MODE=notify ./scripts/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Для полностью автоматических обновлений используйте `UPDATE_MODE=auto`.
|
||||||
|
Приватному репозиторию нужен read-only API-токен Gitea:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GITEA_TOKEN='read-only-token' UPDATE_MODE=auto ./scripts/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Токен сохраняется с правами `0600` в `/etc/proxmox-dashboard/update.env`.
|
||||||
|
Проверить обновления вручную можно командой:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl start proxmox-dashboard-update.service
|
||||||
|
systemctl status proxmox-dashboard-update.service
|
||||||
|
```
|
||||||
|
|
||||||
Откройте:
|
Откройте:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ func evaluateAlerts(metrics dashboardMetrics, thresholds AlertThresholds) []Aler
|
|||||||
add := func(id, severity, source, title, message string) {
|
add := func(id, severity, source, title, message string) {
|
||||||
alerts = append(alerts, Alert{ID: id, Severity: severity, Source: source, Title: title, Message: message})
|
alerts = append(alerts, Alert{ID: id, Severity: severity, Source: source, Title: title, Message: message})
|
||||||
}
|
}
|
||||||
|
if metrics.Update.State == "available" {
|
||||||
|
add("dashboard-update", "info", "Dashboard", "Доступно обновление "+metrics.Update.AvailableVersion, "Режим обновлений: только уведомлять. Установите обновление с сервера или включите автоматический режим.")
|
||||||
|
} else if metrics.Update.State == "error" {
|
||||||
|
add("dashboard-update-error", "warning", "Dashboard", "Не удалось проверить обновления", metrics.Update.Message)
|
||||||
|
}
|
||||||
|
|
||||||
if metrics.CPU.UsagePercent >= thresholds.CPUCritical {
|
if metrics.CPU.UsagePercent >= thresholds.CPUCritical {
|
||||||
add("cpu-usage", "critical", "CPU", "Очень высокая загрузка CPU", fmt.Sprintf("Текущая загрузка %.1f%%.", metrics.CPU.UsagePercent))
|
add("cpu-usage", "critical", "CPU", "Очень высокая загрузка CPU", fmt.Sprintf("Текущая загрузка %.1f%%.", metrics.CPU.UsagePercent))
|
||||||
|
|||||||
8
main.go
8
main.go
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -18,6 +19,13 @@ const defaultDatabasePath = "/var/lib/proxmox-dashboard/dashboard.db"
|
|||||||
var version = "dev"
|
var version = "dev"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
showVersion := flag.Bool("version", false, "show version")
|
||||||
|
flag.Parse()
|
||||||
|
if *showVersion {
|
||||||
|
fmt.Println(version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
address := os.Getenv("DASHBOARD_ADDR")
|
address := os.Getenv("DASHBOARD_ADDR")
|
||||||
if address == "" {
|
if address == "" {
|
||||||
address = defaultAddress
|
address = defaultAddress
|
||||||
|
|||||||
10
proxmox-dashboard-update.service
Normal file
10
proxmox-dashboard-update.service
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Check and install Proxmox Dashboard updates
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/sbin/proxmox-dashboard-update
|
||||||
|
Nice=10
|
||||||
|
IOSchedulingClass=idle
|
||||||
11
proxmox-dashboard-update.timer
Normal file
11
proxmox-dashboard-update.timer
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Check Proxmox Dashboard updates daily
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=10min
|
||||||
|
OnUnitActiveSec=24h
|
||||||
|
RandomizedDelaySec=30min
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
34
scripts/install.sh
Executable file
34
scripts/install.sh
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo "Запустите установщик от root." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SOURCE_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
UPDATE_MODE=${UPDATE_MODE:-notify}
|
||||||
|
GITEA_URL=${GITEA_URL:-https://git.myown.center}
|
||||||
|
GITEA_REPOSITORY=${GITEA_REPOSITORY:-maxim/ProxmoxDash}
|
||||||
|
GITEA_TOKEN=${GITEA_TOKEN:-}
|
||||||
|
|
||||||
|
install -d -m 0750 /etc/proxmox-dashboard /var/lib/proxmox-dashboard
|
||||||
|
install -m 0755 "$SOURCE_DIR/dist/proxmox-dashboard-linux-amd64" /usr/local/bin/proxmox-cpu-dashboard
|
||||||
|
install -m 0755 "$SOURCE_DIR/scripts/proxmox-dashboard-update" /usr/local/sbin/proxmox-dashboard-update
|
||||||
|
install -m 0644 "$SOURCE_DIR/proxmox-cpu-dashboard.service" /etc/systemd/system/proxmox-cpu-dashboard.service
|
||||||
|
install -m 0644 "$SOURCE_DIR/proxmox-dashboard-update.service" /etc/systemd/system/proxmox-dashboard-update.service
|
||||||
|
install -m 0644 "$SOURCE_DIR/proxmox-dashboard-update.timer" /etc/systemd/system/proxmox-dashboard-update.timer
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
{
|
||||||
|
printf 'GITEA_URL=%s\n' "$GITEA_URL"
|
||||||
|
printf 'GITEA_REPOSITORY=%s\n' "$GITEA_REPOSITORY"
|
||||||
|
printf 'GITEA_TOKEN=%s\n' "$GITEA_TOKEN"
|
||||||
|
printf 'UPDATE_MODE=%s\n' "$UPDATE_MODE"
|
||||||
|
} > /etc/proxmox-dashboard/update.env
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now proxmox-cpu-dashboard.service
|
||||||
|
systemctl enable --now proxmox-dashboard-update.timer
|
||||||
|
systemctl start proxmox-dashboard-update.service
|
||||||
|
echo "Proxmox Dashboard установлен. Откройте http://$(hostname -I | awk '{print $1}'):9105"
|
||||||
104
scripts/proxmox-dashboard-update
Executable file
104
scripts/proxmox-dashboard-update
Executable file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
CONFIG_FILE=${DASHBOARD_UPDATE_CONFIG:-/etc/proxmox-dashboard/update.env}
|
||||||
|
STATUS_FILE=${DASHBOARD_UPDATE_STATUS_PATH:-/var/lib/proxmox-dashboard/update-status.json}
|
||||||
|
BINARY_PATH=${DASHBOARD_BINARY_PATH:-/usr/local/bin/proxmox-cpu-dashboard}
|
||||||
|
|
||||||
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
GITEA_URL=${GITEA_URL:-https://git.myown.center}
|
||||||
|
GITEA_REPOSITORY=${GITEA_REPOSITORY:-maxim/ProxmoxDash}
|
||||||
|
GITEA_TOKEN=${GITEA_TOKEN:-}
|
||||||
|
UPDATE_MODE=${UPDATE_MODE:-notify}
|
||||||
|
ASSET_NAME=${ASSET_NAME:-proxmox-dashboard-linux-amd64}
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$STATUS_FILE")"
|
||||||
|
tmp_dir=$(mktemp -d /tmp/proxmox-dashboard-update.XXXXXX)
|
||||||
|
trap 'rm -rf "$tmp_dir"' EXIT INT TERM
|
||||||
|
|
||||||
|
auth_header=""
|
||||||
|
if [ -n "$GITEA_TOKEN" ]; then
|
||||||
|
auth_header="Authorization: token $GITEA_TOKEN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
api_get() {
|
||||||
|
if [ -n "$auth_header" ]; then
|
||||||
|
curl --fail --silent --show-error --location --header "$auth_header" "$1"
|
||||||
|
else
|
||||||
|
curl --fail --silent --show-error --location "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
write_status() {
|
||||||
|
python3 - "$STATUS_FILE" "$1" "$2" "$3" "$4" "$5" "$6" <<'PY'
|
||||||
|
import datetime, json, os, sys, tempfile
|
||||||
|
path, current, available, state, mode, message, release_url = sys.argv[1:]
|
||||||
|
payload = {
|
||||||
|
"currentVersion": current,
|
||||||
|
"availableVersion": available,
|
||||||
|
"state": state,
|
||||||
|
"mode": mode,
|
||||||
|
"message": message,
|
||||||
|
"releaseUrl": release_url,
|
||||||
|
"checkedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
fd, temporary = tempfile.mkstemp(dir=os.path.dirname(path), prefix=".update-status-")
|
||||||
|
with os.fdopen(fd, "w") as stream:
|
||||||
|
json.dump(payload, stream)
|
||||||
|
os.chmod(temporary, 0o644)
|
||||||
|
os.replace(temporary, path)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
current_version=$($BINARY_PATH -version 2>/dev/null || printf 'dev')
|
||||||
|
release_json=$(api_get "$GITEA_URL/api/v1/repos/$GITEA_REPOSITORY/releases/latest")
|
||||||
|
printf '%s' "$release_json" > "$tmp_dir/release.json"
|
||||||
|
release_data=$(python3 - "$tmp_dir/release.json" "$ASSET_NAME" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
release = json.load(open(sys.argv[1]))
|
||||||
|
asset_name = sys.argv[2]
|
||||||
|
assets = {item.get("name"): item.get("browser_download_url") for item in release.get("assets", [])}
|
||||||
|
binary = assets.get(asset_name, "")
|
||||||
|
checksum = assets.get(asset_name + ".sha256", "")
|
||||||
|
print(release.get("tag_name", ""))
|
||||||
|
print(release.get("html_url", ""))
|
||||||
|
print(binary)
|
||||||
|
print(checksum)
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
latest_version=$(printf '%s\n' "$release_data" | sed -n '1p')
|
||||||
|
release_url=$(printf '%s\n' "$release_data" | sed -n '2p')
|
||||||
|
binary_url=$(printf '%s\n' "$release_data" | sed -n '3p')
|
||||||
|
checksum_url=$(printf '%s\n' "$release_data" | sed -n '4p')
|
||||||
|
|
||||||
|
if [ -z "$latest_version" ] || [ -z "$binary_url" ]; then
|
||||||
|
write_status "$current_version" "$latest_version" error "$UPDATE_MODE" "В свежем релизе не найден бинарник $ASSET_NAME." "$release_url"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
newest=$(printf '%s\n%s\n' "$current_version" "$latest_version" | sort -V | tail -n 1)
|
||||||
|
if [ "$current_version" = "$latest_version" ] || [ "$newest" = "$current_version" ]; then
|
||||||
|
write_status "$current_version" "$latest_version" current "$UPDATE_MODE" "Установлена свежая версия." "$release_url"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$UPDATE_MODE" != "auto" ]; then
|
||||||
|
write_status "$current_version" "$latest_version" available "$UPDATE_MODE" "Доступна новая версия $latest_version." "$release_url"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
api_get "$binary_url" > "$tmp_dir/$ASSET_NAME"
|
||||||
|
if [ -n "$checksum_url" ]; then
|
||||||
|
api_get "$checksum_url" > "$tmp_dir/$ASSET_NAME.sha256"
|
||||||
|
(cd "$tmp_dir" && sha256sum -c "$ASSET_NAME.sha256")
|
||||||
|
fi
|
||||||
|
chmod 0755 "$tmp_dir/$ASSET_NAME"
|
||||||
|
file "$tmp_dir/$ASSET_NAME" | grep -q 'ELF 64-bit'
|
||||||
|
cp -p "$BINARY_PATH" "$BINARY_PATH.previous"
|
||||||
|
install -m 0755 "$tmp_dir/$ASSET_NAME" "$BINARY_PATH"
|
||||||
|
write_status "$latest_version" "$latest_version" updated "$UPDATE_MODE" "Обновление $latest_version установлено." "$release_url"
|
||||||
|
systemctl restart proxmox-cpu-dashboard.service
|
||||||
33
update.go
Normal file
33
update.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultUpdateStatusPath = "/var/lib/proxmox-dashboard/update-status.json"
|
||||||
|
|
||||||
|
type UpdateStatus struct {
|
||||||
|
CurrentVersion string `json:"currentVersion"`
|
||||||
|
AvailableVersion string `json:"availableVersion"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
ReleaseURL string `json:"releaseUrl"`
|
||||||
|
CheckedAt time.Time `json:"checkedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func readUpdateStatus() UpdateStatus {
|
||||||
|
status := UpdateStatus{CurrentVersion: version, State: "unknown", Mode: "notify"}
|
||||||
|
path := os.Getenv("DASHBOARD_UPDATE_STATUS_PATH")
|
||||||
|
if path == "" {
|
||||||
|
path = defaultUpdateStatusPath
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil || json.Unmarshal(data, &status) != nil {
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
status.CurrentVersion = version
|
||||||
|
return status
|
||||||
|
}
|
||||||
27
update_test.go
Normal file
27
update_test.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadUpdateStatus(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "status.json")
|
||||||
|
if err := os.WriteFile(path, []byte(`{"currentVersion":"old","availableVersion":"v2.0.0","state":"available","mode":"notify","message":"Доступно обновление"}`), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("DASHBOARD_UPDATE_STATUS_PATH", path)
|
||||||
|
status := readUpdateStatus()
|
||||||
|
if status.CurrentVersion != version || status.AvailableVersion != "v2.0.0" || status.State != "available" {
|
||||||
|
t.Fatalf("unexpected update status: %+v", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadUpdateStatusWithoutFile(t *testing.T) {
|
||||||
|
t.Setenv("DASHBOARD_UPDATE_STATUS_PATH", filepath.Join(t.TempDir(), "missing.json"))
|
||||||
|
status := readUpdateStatus()
|
||||||
|
if status.CurrentVersion != version || status.State != "unknown" {
|
||||||
|
t.Fatalf("unexpected default update status: %+v", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
13
web.go
13
web.go
@@ -20,6 +20,7 @@ type dashboardMetrics struct {
|
|||||||
Guests GuestsMetrics `json:"guests"`
|
Guests GuestsMetrics `json:"guests"`
|
||||||
Backups BackupMetrics `json:"backups"`
|
Backups BackupMetrics `json:"backups"`
|
||||||
Alerts []Alert `json:"alerts"`
|
Alerts []Alert `json:"alerts"`
|
||||||
|
Update UpdateStatus `json:"update"`
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ func collectDashboard(cpuCollector *cpuCollector, networkCollector *networkColle
|
|||||||
CPU: cpu, Memory: memory, Storage: storage,
|
CPU: cpu, Memory: memory, Storage: storage,
|
||||||
Disks: diskCollector.collect(), Network: network,
|
Disks: diskCollector.collect(), Network: network,
|
||||||
ZFS: zfsCollector.collect(), UPS: upsCollector.collect(),
|
ZFS: zfsCollector.collect(), UPS: upsCollector.collect(),
|
||||||
Guests: guestCollector.collect(), Backups: backupCollector.collect(), Timestamp: time.Now(),
|
Guests: guestCollector.collect(), Backups: backupCollector.collect(), Update: readUpdateStatus(), Timestamp: time.Now(),
|
||||||
}
|
}
|
||||||
metrics.Alerts = evaluateAlerts(metrics, store.Thresholds())
|
metrics.Alerts = evaluateAlerts(metrics, store.Thresholds())
|
||||||
return metrics, nil
|
return metrics, nil
|
||||||
@@ -75,6 +76,7 @@ func routes(collector *cpuCollector, store *Store) http.Handler {
|
|||||||
writeJSON(w, metrics, err)
|
writeJSON(w, metrics, err)
|
||||||
})
|
})
|
||||||
mux.HandleFunc("/api/version", func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, map[string]string{"version": version}, nil) })
|
mux.HandleFunc("/api/version", func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, map[string]string{"version": version}, nil) })
|
||||||
|
mux.HandleFunc("/api/update/status", func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, readUpdateStatus(), nil) })
|
||||||
mux.HandleFunc("/api/system", func(w http.ResponseWriter, _ *http.Request) {
|
mux.HandleFunc("/api/system", func(w http.ResponseWriter, _ *http.Request) {
|
||||||
if networkErr != nil {
|
if networkErr != nil {
|
||||||
writeJSON(w, nil, networkErr)
|
writeJSON(w, nil, networkErr)
|
||||||
@@ -314,7 +316,7 @@ footer{color:#657581;font-size:12px;margin-top:17px}
|
|||||||
|
|
||||||
<dialog id="settingsDialog">
|
<dialog id="settingsDialog">
|
||||||
<div class="modal-head"><div><div class="label">Dashboard</div><h2>Настройки</h2></div><button class="close" aria-label="Закрыть">×</button></div>
|
<div class="modal-head"><div><div class="label">Dashboard</div><h2>Настройки</h2></div><button class="close" aria-label="Закрыть">×</button></div>
|
||||||
<div class="settings-tabs"><button class="settings-tab active" data-settings-tab="appearance">Интерфейс</button><button class="settings-tab" data-settings-tab="alerts">Уведомления</button></div>
|
<div class="settings-tabs"><button class="settings-tab active" data-settings-tab="appearance">Интерфейс</button><button class="settings-tab" data-settings-tab="alerts">Уведомления</button><button class="settings-tab" data-settings-tab="updates">Обновления</button></div>
|
||||||
<div class="settings-body settings-panel" data-settings-panel="appearance">
|
<div class="settings-body settings-panel" data-settings-panel="appearance">
|
||||||
<div class="settings-group"><div class="label">Тема оформления</div><div class="theme-options"><button class="theme-option" data-theme-choice="dark">Тёмная</button><button class="theme-option" data-theme-choice="light">Светлая</button></div></div>
|
<div class="settings-group"><div class="label">Тема оформления</div><div class="theme-options"><button class="theme-option" data-theme-choice="dark">Тёмная</button><button class="theme-option" data-theme-choice="light">Светлая</button></div></div>
|
||||||
<div class="settings-group"><div class="label">Видимые карточки</div><div class="setting-list">
|
<div class="settings-group"><div class="label">Видимые карточки</div><div class="setting-list">
|
||||||
@@ -338,6 +340,12 @@ footer{color:#657581;font-size:12px;margin-top:17px}
|
|||||||
</div>
|
</div>
|
||||||
<button class="save-settings" id="saveThresholds">Сохранить пороги</button><div class="settings-note" id="thresholdStatus">Настройки хранятся на сервере и применяются для всех браузеров.</div>
|
<button class="save-settings" id="saveThresholds">Сохранить пороги</button><div class="settings-note" id="thresholdStatus">Настройки хранятся на сервере и применяются для всех браузеров.</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-body settings-panel" data-settings-panel="updates" hidden>
|
||||||
|
<div class="detail-grid">
|
||||||
|
<div class="detail"><span class="label">Установлена</span><strong id="updateCurrent">—</strong></div><div class="detail"><span class="label">Доступна</span><strong id="updateAvailable">—</strong></div><div class="detail"><span class="label">Режим</span><strong id="updateMode">—</strong></div>
|
||||||
|
</div>
|
||||||
|
<div class="wide"><div class="label">Состояние обновлений</div><div class="temps"><span class="temp" id="updateMessage">Проверка ещё не выполнялась</span></div><div class="settings-note" id="updateChecked">Проверка запускается автоматически раз в сутки.</div></div>
|
||||||
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -348,6 +356,7 @@ const safe=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'
|
|||||||
const uptime=s=>{const d=Math.floor(s/86400),h=Math.floor(s%86400/3600),m=Math.floor(s%3600/60);return d?d+'д '+h+'ч':h?h+'ч '+m+'м':m+'м'};
|
const uptime=s=>{const d=Math.floor(s/86400),h=Math.floor(s%86400/3600),m=Math.floor(s%3600/60);return d?d+'д '+h+'ч':h?h+'ч '+m+'м':m+'м'};
|
||||||
let alertSettings={backupMaxAgeHours:48};
|
let alertSettings={backupMaxAgeHours:48};
|
||||||
function render(x){const c=x.cpu,m=x.memory,s=x.storage,n=x.network,z=x.zfs||{available:false,pools:[]},u=x.ups||{available:false},g=x.guests||{available:false,guests:[]},b=x.backups||{available:false,items:[]},alerts=x.alerts||[],disks=x.disks||[],avg=c.temperatures.length?c.temperatures.reduce((a,t)=>a+t.celsius,0)/c.temperatures.length:null;
|
function render(x){const c=x.cpu,m=x.memory,s=x.storage,n=x.network,z=x.zfs||{available:false,pools:[]},u=x.ups||{available:false},g=x.guests||{available:false,guests:[]},b=x.backups||{available:false,items:[]},alerts=x.alerts||[],disks=x.disks||[],avg=c.temperatures.length?c.temperatures.reduce((a,t)=>a+t.celsius,0)/c.temperatures.length:null;
|
||||||
|
const update=x.update||{};el('updateCurrent').textContent=update.currentVersion||'—';el('updateAvailable').textContent=update.availableVersion||'—';el('updateMode').textContent=update.mode==='auto'?'Автоматически':'Уведомлять';el('updateMessage').textContent=update.message||'Проверка ещё не выполнялась';el('updateChecked').textContent=update.checkedAt?'Последняя проверка: '+new Date(update.checkedAt).toLocaleString('ru-RU'):'Проверка запускается автоматически раз в сутки.';
|
||||||
const critical=alerts.filter(a=>a.severity==='critical').length,warnings=alerts.filter(a=>a.severity==='warning').length,infos=alerts.filter(a=>a.severity==='info').length;el('alertCount').textContent=alerts.length;el('alertCritical').textContent=critical;el('alertWarnings').textContent=warnings;el('alertHeadline').textContent=critical?'Требуется срочное внимание':warnings?'Есть предупреждения':infos?'Есть информационные события':'Система в норме';el('alertBar').style.width=(critical?100:warnings?65:infos?30:0)+'%';
|
const critical=alerts.filter(a=>a.severity==='critical').length,warnings=alerts.filter(a=>a.severity==='warning').length,infos=alerts.filter(a=>a.severity==='info').length;el('alertCount').textContent=alerts.length;el('alertCritical').textContent=critical;el('alertWarnings').textContent=warnings;el('alertHeadline').textContent=critical?'Требуется срочное внимание':warnings?'Есть предупреждения':infos?'Есть информационные события':'Система в норме';el('alertBar').style.width=(critical?100:warnings?65:infos?30:0)+'%';
|
||||||
el('alertList').innerHTML=alerts.length?alerts.map(a=>'<article class="alert-item '+safe(a.severity)+'"><i class="alert-icon"></i><div><div class="alert-source">'+safe(a.source)+' · '+(a.severity==='critical'?'Критическое':a.severity==='warning'?'Предупреждение':'Информация')+'</div><div class="alert-title">'+safe(a.title)+'</div><div class="alert-message">'+safe(a.message)+'</div></div></article>').join(''):'<div class="all-good"><strong>Всё в порядке</strong><span>Активных предупреждений сейчас нет.</span></div>';
|
el('alertList').innerHTML=alerts.length?alerts.map(a=>'<article class="alert-item '+safe(a.severity)+'"><i class="alert-icon"></i><div><div class="alert-source">'+safe(a.source)+' · '+(a.severity==='critical'?'Критическое':a.severity==='warning'?'Предупреждение':'Информация')+'</div><div class="alert-title">'+safe(a.title)+'</div><div class="alert-message">'+safe(a.message)+'</div></div></article>').join(''):'<div class="all-good"><strong>Всё в порядке</strong><span>Активных предупреждений сейчас нет.</span></div>';
|
||||||
el('cpuName').textContent=el('cpuDetailName').textContent=c.model;el('cpuUsage').textContent=fixed(c.usagePercent);el('cpuBar').style.width=Math.min(c.usagePercent,100)+'%';el('cpuTemp').textContent=avg===null?'Нет датчика':fixed(avg)+' °C';el('cpuTopology').textContent=c.physicalCores+' / '+c.logicalCpus;
|
el('cpuName').textContent=el('cpuDetailName').textContent=c.model;el('cpuUsage').textContent=fixed(c.usagePercent);el('cpuBar').style.width=Math.min(c.usagePercent,100)+'%';el('cpuTemp').textContent=avg===null?'Нет датчика':fixed(avg)+' °C';el('cpuTopology').textContent=c.physicalCores+' / '+c.logicalCpus;
|
||||||
|
|||||||
Reference in New Issue
Block a user