diff --git a/internal/server/ai.go b/internal/server/ai.go index 0b8cde4..ae8fd74 100644 --- a/internal/server/ai.go +++ b/internal/server/ai.go @@ -518,12 +518,8 @@ func (s *Server) handleSetAI(w http.ResponseWriter, r *http.Request) { _ = s.db.SetConfig(cfgAIType, typ) _ = s.db.SetConfig(cfgAIURL, strings.TrimSpace(r.FormValue("ai_url"))) _ = s.db.SetConfig(cfgAIModel, strings.TrimSpace(r.FormValue("ai_model"))) - if k := strings.TrimSpace(r.FormValue("ai_key")); k != "" { - _ = s.db.SetConfig(cfgAIKey, k) - } - if r.FormValue("ai_key_clear") == "1" { - _ = s.db.SetConfig(cfgAIKey, "") - } + // The field shows the key there; what it holds is the key (empty: none). + _ = s.db.SetConfig(cfgAIKey, strings.TrimSpace(r.FormValue("ai_key"))) http.Redirect(w, r, "/settings#ai", http.StatusSeeOther) } diff --git a/internal/server/frag_test.go b/internal/server/frag_test.go index 6c820ed..8791e9d 100644 --- a/internal/server/frag_test.go +++ b/internal/server/frag_test.go @@ -57,7 +57,7 @@ func TestFragmentHasNoChrome(t *testing.T) { func TestShellRoutes(t *testing.T) { s, cookie := newTestServer(t) - for _, p := range []string{"/", "/terminal", "/settings", "/agents/x1/files", "/agents/x1/terminal"} { + for _, p := range []string{"/", "/workspace", "/terminal", "/settings", "/agents/x1/files", "/agents/x1/terminal"} { if w := get(t, s, cookie, p); !strings.Contains(w.Body.String(), `id="app"`) { t.Errorf("%s: not the shell (code %d)", p, w.Code) } @@ -105,3 +105,13 @@ func TestRenameStaysOnEdit(t *testing.T) { t.Fatalf("rename: %d to %q", w.Code, loc) } } + +// Настройки show the assistant's model as it is set, key included. +func TestSettingsShowAI(t *testing.T) { + s, c := newTestServer(t) + _ = s.db.SetConfig(cfgAIKey, "key-123") + body := get(t, s, c, "/settings?frag=1").Body.String() + if !strings.Contains(body, `value="key-123"`) || !strings.Contains(body, `value="`+DefaultAIURL+`"`) { + t.Fatal("the AI card is empty") + } +} diff --git a/internal/server/handlers_panel.go b/internal/server/handlers_panel.go index 0fc133f..7c9b250 100644 --- a/internal/server/handlers_panel.go +++ b/internal/server/handlers_panel.go @@ -208,7 +208,6 @@ func (s *Server) handleAgentEdit(w http.ResponseWriter, r *http.Request) { "IsAdmin": s.currentAdmin(r).IsAdmin(), "CSRF": csrfToken(r), "ServerVersion": Version, - "AI": s.aiConfig(), "Outdated": a.Version != "" && a.Version != Version, "Updated": r.URL.Query().Get("updated") == "1", "UpdateMsg": r.URL.Query().Get("umsg"), @@ -444,6 +443,7 @@ func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) { "InstallCurl": fmt.Sprintf("curl -fsSL %s/install.sh | sudo HOST=%s ENROLL_SECRET=%s sh", host, host, secret), "InstallWget": fmt.Sprintf("wget -qO- %s/install.sh | sudo HOST=%s ENROLL_SECRET=%s sh", host, host, secret), "ServerVersion": Version, + "AI": s.aiConfig(), "DBNow": dbNow, "DBChosen": dbChosen, "UfwInstalled": ufwInstalled, diff --git a/internal/server/server.go b/internal/server/server.go index 51fd05b..5bfe2e8 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -227,7 +227,8 @@ func (s *Server) Handler() http.Handler { // Panel (admin-only). mux.Handle("GET /{$}", s.requireAdmin(s.handleDashboard)) - mux.Handle("GET /terminal", s.requireAdmin(s.handleShell)) + mux.Handle("GET /workspace", s.requireAdmin(s.handleShell)) + mux.Handle("GET /terminal", s.requireAdmin(s.handleShell)) // the old address mux.Handle("GET /agents/{id}/terminal", s.requireAdmin(s.handleShell)) mux.Handle("GET /agents/{id}/{kind}", s.requireAdmin(s.handleShell)) // files mux.Handle("GET /agents/{id}/forwards", s.requireAdmin(s.handleForwardsPage)) diff --git a/internal/server/web/static/ai.js b/internal/server/web/static/ai.js index e08d2a3..f9308e4 100644 --- a/internal/server/web/static/ai.js +++ b/internal/server/web/static/ai.js @@ -225,7 +225,7 @@ if (act && act.term && !act.ssh && act.agent.id === m.id) return act; let t = ws.panes().find(p => p.term && !p.ssh && p.agent.id === m.id); if (!t) { - if (document.querySelector('main#app').dataset.view !== 'terminal') App.go('/terminal'); + if (document.querySelector('main#app').dataset.view !== 'terminal') App.go('/workspace'); ws.open('term', {id: m.id, name: m.name, host: m.hostname, user: m.user}); t = ws.panes().find(p => p.term && !p.ssh && p.agent.id === m.id); for (let i = 0; i < 40 && t && t.state !== 'ok'; i++) await sleep(250); diff --git a/internal/server/web/static/app.css b/internal/server/web/static/app.css index ff17e35..c5b640b 100644 --- a/internal/server/web/static/app.css +++ b/internal/server/web/static/app.css @@ -904,3 +904,17 @@ body.ai-dragging .ai-fab, body.ai-dragging .ai-box { transition:none; } .ai-form-in textarea { min-height:40px; } .ai-send { min-height:40px; max-height:40px; min-width:40px; max-width:40px; line-height:1; border-radius:10px; } .ai-form-in textarea { border-radius:10px; } +/* Настройки: the key with its eye; PostgreSQL's fields only when chosen. */ +.ai-form .keybox { display:flex; gap:6px; align-items:center; } +.ai-form .keybox input { flex:1; } +.ai-form .keybox .btn.sq { width:38px; height:38px; flex:none; } +.db-form:not(.pg) .pg-fields { display:none; } +.db-form .pg-fields { display:grid; gap:10px; } +.db-form .frow2 { display:flex; gap:10px; } +.db-form .frow2 label:first-child { flex:1; } +.db-form .frow2 .port { width:120px; } +.db-actions { margin-top:12px; } +.db-actions form { margin:0; } +/* Moving between Машины, WorkSpace and Настройки: the page comes in softly. */ +main#app > .view:not([hidden]) { animation:view-in .22s ease both; } +@keyframes view-in { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:none; } } diff --git a/internal/server/web/static/app.js b/internal/server/web/static/app.js index 6c7a9fb..8b7a58e 100644 --- a/internal/server/web/static/app.js +++ b/internal/server/web/static/app.js @@ -7,7 +7,7 @@ // unmounted - its timers, requests and listeners dropped - when it goes. (function () { 'use strict'; - const views = {'/': 'machines', '/terminal': 'terminal', '/settings': 'settings'}; + const views = {'/': 'machines', '/workspace': 'terminal', '/terminal': 'terminal', '/settings': 'settings'}; const modules = {}; const mounted = new Map(); // area -> {mod, scope} const $ = s => document.querySelector(s); @@ -120,6 +120,8 @@ // start mounts what the server drew, once every script has registered. function start() { + // The old address of the workspace shows as the new one. + if (location.pathname === '/terminal') { history.replaceState({}, '', '/workspace' + location.search); shown = '/workspace' + location.search; } document.querySelectorAll('main#app > .view').forEach(mount); show({dashboard: 'machines', settings: 'settings'}[$('main#app').dataset.view] || 'terminal'); // /agents/{id}/terminal or /files: that pane, at once. diff --git a/internal/server/web/static/workspace.js b/internal/server/web/static/workspace.js index dbc7704..9228acb 100644 --- a/internal/server/web/static/workspace.js +++ b/internal/server/web/static/workspace.js @@ -754,7 +754,7 @@ // open opens a pane of kind on agent in a new tab and shows the view. function open(kind, agent, opts) { const t = openPane(kind, agent, opts); - if (location.pathname !== '/terminal' && window.App) App.go('/terminal'); + if (location.pathname !== '/workspace' && window.App) App.go('/workspace'); return t; } const in_ = (el, x, y) => { const r = el.getBoundingClientRect(); return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom; }; diff --git a/internal/server/web/templates/layout.html b/internal/server/web/templates/layout.html index bf2d0f0..a4e656a 100644 --- a/internal/server/web/templates/layout.html +++ b/internal/server/web/templates/layout.html @@ -9,7 +9,7 @@
LPRA · Удалённый доступ