diff --git a/internal/server/collect_handlers_test.go b/internal/server/collect_handlers_test.go
index e27669b..57a408f 100644
--- a/internal/server/collect_handlers_test.go
+++ b/internal/server/collect_handlers_test.go
@@ -8,6 +8,8 @@ import (
"strings"
"testing"
"time"
+
+ "git.mchus.pro/mchus/logpile/internal/models"
)
func newCollectTestServer() (*Server, *httptest.Server) {
@@ -140,6 +142,85 @@ func TestCollectNotFoundAndSecretLeak(t *testing.T) {
}
}
+func TestCollectStartPreservesCurrentResultUntilSuccess(t *testing.T) {
+ s, ts := newCollectTestServer()
+ defer ts.Close()
+
+ existing := &models.AnalysisResult{
+ Filename: "archive.tar.gz",
+ SourceType: models.SourceTypeArchive,
+ CollectedAt: time.Now().UTC(),
+ }
+ s.SetResult(existing)
+
+ body := `{"host":"bmc-success.local","protocol":"redfish","port":443,"username":"admin","auth_type":"password","password":"secret","tls_mode":"strict"}`
+ resp, err := http.Post(ts.URL+"/api/collect", "application/json", bytes.NewBufferString(body))
+ if err != nil {
+ t.Fatalf("post collect failed: %v", err)
+ }
+ defer resp.Body.Close()
+
+ var created CollectJobResponse
+ if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
+ t.Fatalf("decode create response: %v", err)
+ }
+
+ current := s.GetResult()
+ if current != existing {
+ t.Fatalf("expected current result to stay unchanged before success")
+ }
+
+ status := waitForTerminalStatus(t, ts.URL, created.JobID, 4*time.Second)
+ if status.Status != CollectStatusSuccess {
+ t.Fatalf("expected success, got %q", status.Status)
+ }
+
+ finalResult := s.GetResult()
+ if finalResult == nil {
+ t.Fatalf("expected result to be set on success")
+ }
+ if finalResult.SourceType != models.SourceTypeAPI {
+ t.Fatalf("expected api source type after success, got %q", finalResult.SourceType)
+ }
+ if finalResult.TargetHost != "bmc-success.local" {
+ t.Fatalf("expected target host to be updated, got %q", finalResult.TargetHost)
+ }
+}
+
+func TestCollectFailedDoesNotOverwriteCurrentResult(t *testing.T) {
+ s, ts := newCollectTestServer()
+ defer ts.Close()
+
+ existing := &models.AnalysisResult{
+ Filename: "still-archive.tar.gz",
+ SourceType: models.SourceTypeArchive,
+ CollectedAt: time.Now().UTC(),
+ }
+ s.SetResult(existing)
+
+ body := `{"host":"contains-fail.local","protocol":"redfish","port":443,"username":"admin","auth_type":"password","password":"secret","tls_mode":"strict"}`
+ resp, err := http.Post(ts.URL+"/api/collect", "application/json", bytes.NewBufferString(body))
+ if err != nil {
+ t.Fatalf("post collect failed: %v", err)
+ }
+ defer resp.Body.Close()
+
+ var created CollectJobResponse
+ if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
+ t.Fatalf("decode create response: %v", err)
+ }
+
+ status := waitForTerminalStatus(t, ts.URL, created.JobID, 4*time.Second)
+ if status.Status != CollectStatusFailed {
+ t.Fatalf("expected failed, got %q", status.Status)
+ }
+
+ finalResult := s.GetResult()
+ if finalResult != existing {
+ t.Fatalf("expected existing result to remain on failed job")
+ }
+}
+
func waitForTerminalStatus(t *testing.T, baseURL, jobID string, timeout time.Duration) CollectJobStatusResponse {
t.Helper()
deadline := time.Now().Add(timeout)
diff --git a/internal/server/handlers.go b/internal/server/handlers.go
index e706a81..4ec6134 100644
--- a/internal/server/handlers.go
+++ b/internal/server/handlers.go
@@ -592,8 +592,6 @@ func (s *Server) handleCollectStart(w http.ResponseWriter, r *http.Request) {
}
job := s.jobManager.CreateJob(req)
- s.SetResult(newAPIResult(req))
- s.SetDetectedVendor("")
s.startMockCollectionJob(job.ID, req)
w.Header().Set("Content-Type", "application/json")
@@ -681,6 +679,8 @@ func (s *Server) startMockCollectionJob(jobID string, req CollectRequest) {
s.jobManager.UpdateJobStatus(jobID, CollectStatusSuccess, 100, "")
s.jobManager.AppendJobLog(jobID, "Сбор завершен")
+ s.SetResult(newAPIResult(req))
+ s.SetDetectedVendor("")
}()
}
diff --git a/web/static/js/app.js b/web/static/js/app.js
index 8a12a3f..dd987eb 100644
--- a/web/static/js/app.js
+++ b/web/static/js/app.js
@@ -16,6 +16,8 @@ let collectionJobPollTimer = null;
let collectionJobScenario = [];
let collectionJobScenarioIndex = 0;
let collectionJobLogCounter = 0;
+let apiPortTouchedByUser = false;
+let isAutoUpdatingApiPort = false;
function initSourceType() {
const sourceButtons = document.querySelectorAll('.source-switch-btn');
@@ -49,7 +51,7 @@ function initApiSource() {
const authTypeField = document.getElementById('api-auth-type');
const cancelJobButton = document.getElementById('cancel-job-btn');
- const fieldNames = ['host', 'protocol', 'port', 'username', 'authType', 'password', 'token'];
+ const fieldNames = ['host', 'protocol', 'port', 'username', 'auth_type', 'tls_mode', 'password', 'token'];
apiForm.addEventListener('submit', (event) => {
event.preventDefault();
@@ -65,7 +67,6 @@ function initApiSource() {
apiConnectPayload = payload;
renderApiConnectStatus(true, payload);
startCollectionJob(payload);
- console.log('API payload prepared:', apiConnectPayload);
});
if (cancelJobButton) {
@@ -82,9 +83,15 @@ function initApiSource() {
const eventName = field.tagName.toLowerCase() === 'select' ? 'change' : 'input';
field.addEventListener(eventName, () => {
- if (fieldName === 'authType') {
+ if (fieldName === 'auth_type') {
toggleApiAuthFields(authTypeField.value);
}
+ if (fieldName === 'protocol') {
+ applyProtocolDefaultPort(field.value);
+ }
+ if (fieldName === 'port') {
+ handleApiPortInput(field.value);
+ }
const { errors } = validateCollectForm();
renderFormErrors(errors);
@@ -96,6 +103,7 @@ function initApiSource() {
});
});
+ applyProtocolDefaultPort(getApiValue('protocol'));
toggleApiAuthFields(authTypeField.value);
renderCollectionJob();
}
@@ -105,7 +113,8 @@ function validateCollectForm() {
const protocol = getApiValue('protocol');
const portRaw = getApiValue('port');
const username = getApiValue('username');
- const authType = getApiValue('authType');
+ const authType = getApiValue('auth_type');
+ const tlsMode = getApiValue('tls_mode');
const password = getApiValue('password');
const token = getApiValue('token');
@@ -132,7 +141,10 @@ function validateCollectForm() {
}
if (!['password', 'token'].includes(authType)) {
- errors.authType = 'Выберите тип авторизации.';
+ errors.auth_type = 'Выберите тип авторизации.';
+ }
+ if (!['strict', 'insecure'].includes(tlsMode)) {
+ errors.tls_mode = 'Выберите TLS режим.';
}
if (authType === 'password' && !password) {
@@ -148,20 +160,18 @@ function validateCollectForm() {
}
const payload = {
- sourceType: 'api',
- connection: {
- host,
- protocol,
- port,
- username,
- authType
- }
+ host,
+ protocol,
+ port,
+ username,
+ auth_type: authType,
+ tls_mode: tlsMode
};
if (authType === 'password') {
- payload.connection.password = password;
+ payload.password = password;
} else {
- payload.connection.token = token;
+ payload.token = token;
}
return { isValid: true, errors: {}, payload };
@@ -174,7 +184,7 @@ function renderFormErrors(errors) {
return;
}
- const errorFields = ['host', 'protocol', 'port', 'username', 'authType', 'password', 'token'];
+ const errorFields = ['host', 'protocol', 'port', 'username', 'auth_type', 'tls_mode', 'password', 'token'];
errorFields.forEach((fieldName) => {
const errorNode = apiForm.querySelector(`[data-error-for="${fieldName}"]`);
if (!errorNode) {
@@ -212,12 +222,12 @@ function renderApiConnectStatus(isValid, payload) {
return;
}
- const payloadPreview = { ...payload, connection: { ...payload.connection } };
- if (payloadPreview.connection.password) {
- payloadPreview.connection.password = '***';
+ const payloadPreview = { ...payload };
+ if (payloadPreview.password) {
+ payloadPreview.password = '***';
}
- if (payloadPreview.connection.token) {
- payloadPreview.connection.token = '***';
+ if (payloadPreview.token) {
+ payloadPreview.token = '***';
}
status.textContent = `Payload сформирован: ${JSON.stringify(payloadPreview)}`;
@@ -408,6 +418,43 @@ function toggleApiAuthFields(authType) {
tokenField.classList.toggle('hidden', !useToken);
}
+function applyProtocolDefaultPort(protocol) {
+ const defaults = {
+ redfish: '443',
+ ipmi: '623'
+ };
+ const defaultPort = defaults[protocol];
+ if (!defaultPort) {
+ return;
+ }
+
+ const apiForm = document.getElementById('api-connect-form');
+ if (!apiForm) {
+ return;
+ }
+
+ const portField = apiForm.elements.namedItem('port');
+ if (!portField || typeof portField.value !== 'string') {
+ return;
+ }
+
+ const currentValue = portField.value.trim();
+ if (apiPortTouchedByUser && currentValue !== '') {
+ return;
+ }
+
+ isAutoUpdatingApiPort = true;
+ portField.value = defaultPort;
+ isAutoUpdatingApiPort = false;
+}
+
+function handleApiPortInput(value) {
+ if (isAutoUpdatingApiPort) {
+ return;
+ }
+ apiPortTouchedByUser = value.trim() !== '';
+}
+
function getApiValue(fieldName) {
const apiForm = document.getElementById('api-connect-form');
if (!apiForm) {
diff --git a/web/templates/index.html b/web/templates/index.html
index c4560ea..cf25267 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -66,12 +66,22 @@
+
+