diff --git a/node/controller.go b/node/controller.go index 224c4d6..2a5bde5 100644 --- a/node/controller.go +++ b/node/controller.go @@ -210,6 +210,11 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error { // GetRemoteLogs requests console logs from a remote miner. func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]string, error) { + return c.GetRemoteLogsSince(peerID, minerName, lines, time.Time{}) +} + +// GetRemoteLogsSince requests console logs from a remote miner after a point in time. +func (c *Controller) GetRemoteLogsSince(peerID, minerName string, lines int, since time.Time) ([]string, error) { identity := c.node.GetIdentity() if identity == nil { return nil, ErrIdentityNotInitialized @@ -219,10 +224,13 @@ func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]strin MinerName: minerName, Lines: lines, } + if !since.IsZero() { + payload.Since = since.UnixMilli() + } msg, err := NewMessage(MsgGetLogs, identity.ID, peerID, payload) if err != nil { - return nil, coreerr.E("Controller.GetRemoteLogs", "failed to create message", err) + return nil, coreerr.E("Controller.GetRemoteLogsSince", "failed to create message", err) } resp, err := c.sendRequest(peerID, msg, 10*time.Second) diff --git a/node/controller_test.go b/node/controller_test.go index ee9a383..d2a69aa 100644 --- a/node/controller_test.go +++ b/node/controller_test.go @@ -7,6 +7,7 @@ import ( "net/http/httptest" "net/url" "path/filepath" + "strings" "sync" "sync/atomic" "testing" @@ -514,6 +515,40 @@ type mockMinerFull struct { func (m *mockMinerFull) GetName() string { return m.name } func (m *mockMinerFull) GetType() string { return m.minerType } func (m *mockMinerFull) GetStats() (any, error) { return m.stats, nil } +func (m *mockMinerFull) GetConsoleHistorySince(lines int, since time.Time) []string { + if since.IsZero() { + if lines >= len(m.consoleHistory) { + return m.consoleHistory + } + return m.consoleHistory[:lines] + } + + filtered := make([]string, 0, len(m.consoleHistory)) + for _, line := range m.consoleHistory { + if lineAfter(line, since) { + filtered = append(filtered, line) + } + } + if lines >= len(filtered) { + return filtered + } + return filtered[:lines] +} + +func lineAfter(line string, since time.Time) bool { + start := strings.IndexByte(line, '[') + end := strings.IndexByte(line, ']') + if start != 0 || end <= start+1 { + return true + } + + ts, err := time.Parse("2006-01-02 15:04:05", line[start+1:end]) + if err != nil { + return true + } + return ts.After(since) || ts.Equal(since) +} + func (m *mockMinerFull) GetConsoleHistory(lines int) []string { if lines >= len(m.consoleHistory) { return m.consoleHistory @@ -616,6 +651,20 @@ func TestController_GetRemoteLogs_LimitedLines(t *testing.T) { assert.Len(t, lines, 1, "should return only 1 line") } +func TestController_GetRemoteLogsSince(t *testing.T) { + controller, _, tp := setupControllerPairWithMiner(t) + serverID := tp.ServerNode.GetIdentity().ID + + since, err := time.Parse("2006-01-02 15:04:05", "2026-02-20 10:00:01") + require.NoError(t, err) + + lines, err := controller.GetRemoteLogsSince(serverID, "running-miner", 10, since) + require.NoError(t, err, "GetRemoteLogsSince should succeed") + require.Len(t, lines, 2, "should return only log lines on or after the requested timestamp") + assert.Contains(t, lines[0], "connected to pool") + assert.Contains(t, lines[1], "new job received") +} + func TestController_GetRemoteLogs_NoIdentity(t *testing.T) { tp := setupTestTransportPair(t) nmNoID, err := NewNodeManagerWithPaths( diff --git a/node/worker.go b/node/worker.go index af917d4..d13dcd1 100644 --- a/node/worker.go +++ b/node/worker.go @@ -26,7 +26,7 @@ type MinerInstance interface { GetName() string GetType() string GetStats() (any, error) - GetConsoleHistory(lines int) []string + GetConsoleHistorySince(lines int, since time.Time) []string } // ProfileManager interface for profile operations. @@ -55,7 +55,6 @@ func NewWorker(node *NodeManager, transport *Transport) *Worker { } } - // SetMinerManager sets the miner manager for handling miner operations. func (w *Worker) SetMinerManager(manager MinerManager) { w.minerManager = manager @@ -286,7 +285,12 @@ func (w *Worker) handleGetLogs(msg *Message) (*Message, error) { return nil, coreerr.E("Worker.handleGetLogs", "miner not found: "+payload.MinerName, nil) } - lines := miner.GetConsoleHistory(payload.Lines) + var since time.Time + if payload.Since > 0 { + since = time.UnixMilli(payload.Since) + } + + lines := miner.GetConsoleHistorySince(payload.Lines, since) logs := LogsPayload{ MinerName: payload.MinerName, diff --git a/node/worker_test.go b/node/worker_test.go index ee3ed31..2aeef6d 100644 --- a/node/worker_test.go +++ b/node/worker_test.go @@ -550,10 +550,14 @@ type mockMinerInstance struct { stats any } -func (m *mockMinerInstance) GetName() string { return m.name } -func (m *mockMinerInstance) GetType() string { return m.minerType } -func (m *mockMinerInstance) GetStats() (any, error) { return m.stats, nil } -func (m *mockMinerInstance) GetConsoleHistory(lines int) []string { return []string{} } +func (m *mockMinerInstance) GetName() string { return m.name } +func (m *mockMinerInstance) GetType() string { return m.minerType } +func (m *mockMinerInstance) GetStats() (any, error) { + return m.stats, nil +} +func (m *mockMinerInstance) GetConsoleHistorySince(lines int, since time.Time) []string { + return []string{} +} type mockProfileManager struct{}