diff --git a/README.md b/README.md index 8b66f4e..b3b2161 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,9 @@ Проверка обновлений выполняется отдельным systemd-таймером раз в сутки. Статус виден во вкладке `Настройки → Обновления` и в центре уведомлений. Доступны два режима: `notify` только сообщает о новой версии, `auto` скачивает её, сохраняет -предыдущий бинарник как резервный и перезапускает панель. +предыдущий бинарник как резервный и перезапускает панель. В режиме `notify` +обновление можно запустить кнопкой `Проверить и установить обновление` в +настройках. Запрос выполняет отдельная системная служба. На главном экране находятся компактные карточки CPU, памяти, диска и сети. Нажатие на карточку открывает подробные показатели. diff --git a/proxmox-dashboard-update.path b/proxmox-dashboard-update.path new file mode 100644 index 0000000..98e15d2 --- /dev/null +++ b/proxmox-dashboard-update.path @@ -0,0 +1,9 @@ +[Unit] +Description=Watch for manual Proxmox Dashboard update requests + +[Path] +PathExists=/var/lib/proxmox-dashboard/update-request +Unit=proxmox-dashboard-update.service + +[Install] +WantedBy=multi-user.target diff --git a/scripts/install.sh b/scripts/install.sh index 4a76610..11e2009 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -18,6 +18,7 @@ install -m 0755 "$SOURCE_DIR/scripts/proxmox-dashboard-update" /usr/local/sbin/p 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 +install -m 0644 "$SOURCE_DIR/proxmox-dashboard-update.path" /etc/systemd/system/proxmox-dashboard-update.path umask 077 { @@ -31,5 +32,6 @@ systemctl daemon-reload systemctl enable proxmox-cpu-dashboard.service systemctl restart proxmox-cpu-dashboard.service systemctl enable --now proxmox-dashboard-update.timer +systemctl enable --now proxmox-dashboard-update.path systemctl start proxmox-dashboard-update.service echo "Proxmox Dashboard установлен. Откройте http://$(hostname -I | awk '{print $1}'):9105" diff --git a/scripts/proxmox-dashboard-update b/scripts/proxmox-dashboard-update index 98f4515..50982a6 100755 --- a/scripts/proxmox-dashboard-update +++ b/scripts/proxmox-dashboard-update @@ -4,6 +4,7 @@ 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} +REQUEST_FILE=${DASHBOARD_UPDATE_REQUEST_PATH:-/var/lib/proxmox-dashboard/update-request} if [ -f "$CONFIG_FILE" ]; then # shellcheck disable=SC1090 @@ -15,6 +16,11 @@ GITEA_REPOSITORY=${GITEA_REPOSITORY:-maxim/ProxmoxDash} GITEA_TOKEN=${GITEA_TOKEN:-} UPDATE_MODE=${UPDATE_MODE:-notify} ASSET_NAME=${ASSET_NAME:-proxmox-dashboard-linux-amd64} +manual_request=false +if [ -f "$REQUEST_FILE" ]; then + manual_request=true + rm -f "$REQUEST_FILE" +fi mkdir -p "$(dirname "$STATUS_FILE")" tmp_dir=$(mktemp -d /tmp/proxmox-dashboard-update.XXXXXX) @@ -86,11 +92,12 @@ if [ "$current_version" = "$latest_version" ] || [ "$newest" = "$current_version exit 0 fi -if [ "$UPDATE_MODE" != "auto" ]; then +if [ "$UPDATE_MODE" != "auto" ] && [ "$manual_request" != "true" ]; then write_status "$current_version" "$latest_version" available "$UPDATE_MODE" "Доступна новая версия $latest_version." "$release_url" exit 0 fi +write_status "$current_version" "$latest_version" installing "$UPDATE_MODE" "Скачиваем и проверяем $latest_version." "$release_url" api_get "$binary_url" > "$tmp_dir/$ASSET_NAME" if [ -n "$checksum_url" ]; then api_get "$checksum_url" > "$tmp_dir/$ASSET_NAME.sha256" diff --git a/update.go b/update.go index b2a2fc9..cb4b33a 100644 --- a/update.go +++ b/update.go @@ -2,11 +2,14 @@ package main import ( "encoding/json" + "fmt" "os" + "path/filepath" "time" ) const defaultUpdateStatusPath = "/var/lib/proxmox-dashboard/update-status.json" +const defaultUpdateRequestPath = "/var/lib/proxmox-dashboard/update-request" type UpdateStatus struct { CurrentVersion string `json:"currentVersion"` @@ -31,3 +34,44 @@ func readUpdateStatus() UpdateStatus { status.CurrentVersion = version return status } + +func requestUpdate() (UpdateStatus, error) { + requestPath := os.Getenv("DASHBOARD_UPDATE_REQUEST_PATH") + if requestPath == "" { + requestPath = defaultUpdateRequestPath + } + if err := os.MkdirAll(filepath.Dir(requestPath), 0o750); err != nil { + return UpdateStatus{}, err + } + status := readUpdateStatus() + status.State = "queued" + status.Message = "Обновление поставлено в очередь." + status.CheckedAt = time.Now() + statusPath := os.Getenv("DASHBOARD_UPDATE_STATUS_PATH") + if statusPath == "" { + statusPath = defaultUpdateStatusPath + } + data, _ := json.Marshal(status) + temporary, err := os.CreateTemp(filepath.Dir(statusPath), ".update-status-") + if err != nil { + return UpdateStatus{}, err + } + temporaryPath := temporary.Name() + defer os.Remove(temporaryPath) + if _, err = temporary.Write(data); err == nil { + err = temporary.Chmod(0o644) + } + if closeErr := temporary.Close(); err == nil { + err = closeErr + } + if err == nil { + err = os.Rename(temporaryPath, statusPath) + } + if err != nil { + return UpdateStatus{}, fmt.Errorf("не удалось сохранить запрос обновления: %w", err) + } + if err := os.WriteFile(requestPath, []byte(time.Now().Format(time.RFC3339Nano)), 0o640); err != nil { + return UpdateStatus{}, err + } + return status, nil +} diff --git a/update_test.go b/update_test.go index 5e456f9..ba18fcf 100644 --- a/update_test.go +++ b/update_test.go @@ -25,3 +25,21 @@ func TestReadUpdateStatusWithoutFile(t *testing.T) { t.Fatalf("unexpected default update status: %+v", status) } } + +func TestRequestUpdate(t *testing.T) { + directory := t.TempDir() + statusPath := filepath.Join(directory, "status.json") + requestPath := filepath.Join(directory, "request") + t.Setenv("DASHBOARD_UPDATE_STATUS_PATH", statusPath) + t.Setenv("DASHBOARD_UPDATE_REQUEST_PATH", requestPath) + status, err := requestUpdate() + if err != nil { + t.Fatal(err) + } + if status.State != "queued" { + t.Fatalf("unexpected queued status: %+v", status) + } + if _, err := os.Stat(requestPath); err != nil { + t.Fatalf("update request was not created: %v", err) + } +} diff --git a/web.go b/web.go index 75da913..1b4c648 100644 --- a/web.go +++ b/web.go @@ -77,6 +77,14 @@ func routes(collector *cpuCollector, store *Store) http.Handler { }) 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/update/request", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + status, err := requestUpdate() + writeJSON(w, status, err) + }) mux.HandleFunc("/api/system", func(w http.ResponseWriter, _ *http.Request) { if networkErr != nil { writeJSON(w, nil, networkErr) @@ -178,6 +186,7 @@ dialog{width:min(760px,calc(100% - 28px));max-height:calc(100vh - 32px);padding: html[data-theme="light"] body{background:radial-gradient(circle at 78% -8%,#cde9da 0,transparent 34%),var(--bg)}html[data-theme="light"] .card{background:linear-gradient(145deg,#fff,#f7f9fa)}html[data-theme="light"] dialog{background:#fff;box-shadow:0 30px 80px #4e64752b}html[data-theme="light"] .modal-head{background:rgba(255,255,255,.94)}html[data-theme="light"] .close{background:#f1f4f6}html[data-theme="light"] .disk-item,html[data-theme="light"] .alert-item{background:#f7f9fa}html[data-theme="light"] .bar{background:#e2e8ec}html[data-theme="light"] dialog::backdrop{background:rgba(30,45,55,.35)}html[data-theme="light"] .badge[data-reason]:hover::after,html[data-theme="light"] .badge[data-reason]:focus::after,html[data-theme="light"] .chart-tooltip{background:#fff;box-shadow:0 12px 32px #4e647533} footer{color:#657581;font-size:12px;margin-top:17px} @media(max-width:700px){main{padding:28px 0}.cards{grid-template-columns:1fr}header{align-items:flex-start;flex-direction:column;gap:14px}.detail-grid{grid-template-columns:repeat(2,1fr)}.detail:nth-child(3n){border-right:1px solid var(--line)}.detail:nth-child(2n){border-right:0}.name{max-width:220px}.disk-stats,.setting-list,.threshold-grid{grid-template-columns:repeat(2,1fr)}.chart-head{align-items:flex-start;flex-direction:column}.detail-tabs{overflow-x:auto}} +.save-settings:disabled{cursor:wait;opacity:.55}