From c26c41e6a158bd83f9d0787d9f2302451a96996b Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sun, 5 Apr 2026 01:50:19 +0300 Subject: [PATCH] feat: enable proxy toggle on quick deploy, event log clearing, and UX fixes - Add enable_proxy toggle to Quick Deploy form (defaults to on) - Add DELETE /api/events/log/{id} and DELETE /api/events/log endpoints - Add Clear All button with confirmation on Events page - Rename "NPM Proxy" to "Enable Proxy" on stage form (provider-agnostic) - Fix polling interval validation (min 60s) and number input trim errors - Fix domain field no longer required in settings --- internal/api/deploys.go | 18 ++++++---- internal/api/eventlog.go | 28 ++++++++++++++++ internal/api/router.go | 4 +++ internal/store/eventlog.go | 22 ++++++++++++ web/src/lib/api.ts | 9 +++++ web/src/lib/i18n/en.json | 6 ++++ web/src/lib/i18n/ru.json | 6 ++++ web/src/routes/deploy/+page.svelte | 9 ++++- web/src/routes/events/+page.svelte | 41 ++++++++++++++++++++--- web/src/routes/projects/[id]/+page.svelte | 4 +-- 10 files changed, 134 insertions(+), 13 deletions(-) diff --git a/internal/api/deploys.go b/internal/api/deploys.go index 1eda118..b1f5678 100644 --- a/internal/api/deploys.go +++ b/internal/api/deploys.go @@ -96,12 +96,13 @@ func (s *Server) inspectImage(w http.ResponseWriter, r *http.Request) { // quickDeployRequest is the expected JSON body for POST /api/deploy/quick. type quickDeployRequest struct { - Name string `json:"name"` - Image string `json:"image"` - Tag string `json:"tag"` - Registry string `json:"registry"` - Port int `json:"port"` - Force bool `json:"force"` // skip duplicate check + Name string `json:"name"` + Image string `json:"image"` + Tag string `json:"tag"` + Registry string `json:"registry"` + Port int `json:"port"` + Force bool `json:"force"` // skip duplicate check + EnableProxy *bool `json:"enable_proxy"` // nil defaults to true } // quickDeploy handles POST /api/deploy/quick. @@ -167,12 +168,17 @@ func (s *Server) quickDeploy(w http.ResponseWriter, r *http.Request) { } // Create default stage. + enableProxy := true + if req.EnableProxy != nil { + enableProxy = *req.EnableProxy + } stage, err := s.store.CreateStage(store.Stage{ ProjectID: project.ID, Name: "dev", TagPattern: "*", AutoDeploy: true, MaxInstances: 1, + EnableProxy: enableProxy, }) if err != nil { slog.Error("failed to create stage", "error", err) diff --git a/internal/api/eventlog.go b/internal/api/eventlog.go index a4fd025..9b188b0 100644 --- a/internal/api/eventlog.go +++ b/internal/api/eventlog.go @@ -5,6 +5,8 @@ import ( "net/http" "strconv" + "github.com/go-chi/chi/v5" + "github.com/alexei/docker-watcher/internal/store" ) @@ -46,3 +48,29 @@ func (s *Server) getEventLogStats(w http.ResponseWriter, r *http.Request) { respondJSON(w, http.StatusOK, stats) } + +// deleteEvent handles DELETE /api/events/log/{id}. +func (s *Server) deleteEvent(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + if err != nil { + respondError(w, http.StatusBadRequest, "invalid event ID") + return + } + if err := s.store.DeleteEvent(id); err != nil { + slog.Error("failed to delete event", "id", id, "error", err) + respondError(w, http.StatusInternalServerError, "internal server error") + return + } + respondJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) +} + +// clearEvents handles DELETE /api/events/log. +func (s *Server) clearEvents(w http.ResponseWriter, r *http.Request) { + cleared, err := s.store.ClearAllEvents() + if err != nil { + slog.Error("failed to clear events", "error", err) + respondError(w, http.StatusInternalServerError, "internal server error") + return + } + respondJSON(w, http.StatusOK, map[string]any{"status": "cleared", "count": cleared}) +} diff --git a/internal/api/router.go b/internal/api/router.go index 7a12c07..cdee380 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -274,6 +274,10 @@ func (s *Server) Router() chi.Router { // Config export (reveals project/infra details). r.Get("/config/export", s.exportConfig) + // Event log management. + r.Delete("/events/log/{id}", s.deleteEvent) + r.Delete("/events/log", s.clearEvents) + // Auth management. r.Get("/auth/settings", s.getAuthSettings) r.Put("/auth/settings", s.updateAuthSettings) diff --git a/internal/store/eventlog.go b/internal/store/eventlog.go index 1414348..54fd5c9 100644 --- a/internal/store/eventlog.go +++ b/internal/store/eventlog.go @@ -153,6 +153,28 @@ func (s *Store) GetEventStats() (EventLogStats, error) { } // PruneEvents deletes event log entries older than the given number of days. +// DeleteEvent removes a single event log entry by ID. +func (s *Store) DeleteEvent(id int64) error { + result, err := s.db.Exec(`DELETE FROM event_log WHERE id = ?`, id) + if err != nil { + return fmt.Errorf("delete event: %w", err) + } + n, _ := result.RowsAffected() + if n == 0 { + return fmt.Errorf("event %d: %w", id, ErrNotFound) + } + return nil +} + +// ClearAllEvents removes all event log entries. +func (s *Store) ClearAllEvents() (int64, error) { + result, err := s.db.Exec(`DELETE FROM event_log`) + if err != nil { + return 0, fmt.Errorf("clear events: %w", err) + } + return result.RowsAffected() +} + func (s *Store) PruneEvents(olderThanDays int) (int64, error) { if olderThanDays < 1 { return 0, fmt.Errorf("prune events: olderThanDays must be >= 1, got %d", olderThanDays) diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 1fc1955..4a613a2 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -215,6 +215,7 @@ export function quickDeploy(data: { registry?: string; port?: number; force?: boolean; + enable_proxy?: boolean; }): Promise<{ project: Project; status: string }> { return post<{ project: Project; status: string }>('/api/deploy/quick', data); } @@ -540,6 +541,14 @@ export function fetchEventLogStats(): Promise { return get('/api/events/log/stats'); } +export function deleteEvent(id: number): Promise<{ status: string }> { + return del<{ status: string }>(`/api/events/log/${id}`); +} + +export function clearAllEvents(): Promise<{ status: string; count: number }> { + return del<{ status: string; count: number }>('/api/events/log'); +} + // ── Stale Containers ──────────────────────────────────────────────── export function fetchStaleContainers(): Promise { diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 4f17689..e2a52a7 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -102,6 +102,7 @@ "tagPatternHelp": "Glob pattern (e.g., dev-*, v*)", "maxInstances": "Max Instances", "autoDeployLabel": "Auto Deploy", + "enableProxy": "Enable Proxy", "npmProxy": "NPM Proxy", "creating": "Creating...", "createStage": "Create Stage", @@ -658,6 +659,11 @@ "noEventsDesc": "Events will appear here as they occur.", "loadMore": "Load more", "newEvents": "new events", + "clearAll": "Clear All", + "clearAllTitle": "Clear Event Log", + "clearAllMessage": "This will permanently delete all event log entries. This cannot be undone.", + "cleared": "Cleared {count} events", + "clearFailed": "Failed to clear events", "filter": { "severity": "Severity", "source": "Source", diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index cb15fb5..553318b 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -102,6 +102,7 @@ "tagPatternHelp": "Glob-шаблон (напр., dev-*, v*)", "maxInstances": "Макс. экземпляров", "autoDeployLabel": "Авто-деплой", + "enableProxy": "Включить прокси", "npmProxy": "NPM прокси", "creating": "Создание...", "createStage": "Создать стадию", @@ -658,6 +659,11 @@ "noEventsDesc": "События будут отображаться здесь по мере их возникновения.", "loadMore": "Загрузить ещё", "newEvents": "новых событий", + "clearAll": "Очистить всё", + "clearAllTitle": "Очистить журнал событий", + "clearAllMessage": "Все записи журнала событий будут удалены безвозвратно.", + "cleared": "Удалено {count} событий", + "clearFailed": "Не удалось очистить события", "filter": { "severity": "Уровень", "source": "Источник", diff --git a/web/src/routes/deploy/+page.svelte b/web/src/routes/deploy/+page.svelte index 4760c50..d589d9a 100644 --- a/web/src/routes/deploy/+page.svelte +++ b/web/src/routes/deploy/+page.svelte @@ -2,6 +2,7 @@ import { inspectImage, quickDeploy, listRegistries, listRegistryImages, deployInstance } from '$lib/api'; import type { InspectResult, EntityPickerItem, Project } from '$lib/types'; import FormField from '$lib/components/FormField.svelte'; + import ToggleSwitch from '$lib/components/ToggleSwitch.svelte'; import EntityPicker from '$lib/components/EntityPicker.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import { toasts } from '$lib/stores/toast'; @@ -20,6 +21,7 @@ let stage = $state('dev'); let subdomain = $state(''); let envVars = $state(''); + let enableProxy = $state(true); let errors = $state>({}); @@ -140,7 +142,7 @@ if (!validateAll()) return; deploying = true; try { - const result = await quickDeploy({ image: imageUrl.trim(), name: projectName.trim(), port: parseInt(port, 10), force }); + const result = await quickDeploy({ image: imageUrl.trim(), name: projectName.trim(), port: parseInt(port, 10), force, enable_proxy: enableProxy }); toasts.success($t('quickDeploy.deployedSuccess', { name: projectName })); // Redirect to the new project page. if (result.project?.id) { @@ -288,6 +290,11 @@
+ +
+ + {$t('projectDetail.enableProxy')} +
diff --git a/web/src/routes/events/+page.svelte b/web/src/routes/events/+page.svelte index 4ed797d..217648a 100644 --- a/web/src/routes/events/+page.svelte +++ b/web/src/routes/events/+page.svelte @@ -5,7 +5,9 @@