diff --git a/agent/agent.go b/agent/agent.go index 5dcd3b605..4a48a0ed3 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -40,6 +40,7 @@ type Agent struct { systemInfo system.Info // Host system info gpuManager *GPUManager // Manages GPU data cache *systemDataCache // Cache for system stats based on cache time + offlineCache *systemOfflineCache // Cache for offline data storage connectionManager *ConnectionManager // Channel to signal connection events handlerRegistry *HandlerRegistry // Registry for routing incoming messages server *ssh.Server // SSH server @@ -53,8 +54,9 @@ type Agent struct { // If the data directory is not set, it will attempt to find the optimal directory. func NewAgent(dataDir ...string) (agent *Agent, err error) { agent = &Agent{ - fsStats: make(map[string]*system.FsStats), - cache: NewSystemDataCache(), + fsStats: make(map[string]*system.FsStats), + cache: NewSystemDataCache(), + offlineCache: NewSystemOfflineCache(), } // Initialize disk I/O previous counters storage @@ -136,6 +138,11 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) { slog.Debug("Stats", "data", agent.gatherStats(0)) } + // start offline cache filler + if val, exists := GetEnv("OFFLINE_CACHING"); exists && val == "true" { + go agent.fillOfflineCache() + } + return agent, nil } @@ -158,9 +165,11 @@ func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData { return data } + now := time.Now() *data = system.CombinedData{ - Stats: a.getSystemStats(cacheTimeMs), - Info: a.systemInfo, + Stats: a.getSystemStats(cacheTimeMs), + Info: a.systemInfo, + Timestamp: &now, } // slog.Info("System data", "data", data, "cacheTimeMs", cacheTimeMs) @@ -242,3 +251,15 @@ func (a *Agent) getFingerprint() string { return fingerprint } + +func (a *Agent) fillOfflineCache() { + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + + for range ticker.C { + if a.connectionManager.State == Disconnected { + data := a.gatherStats(0) + a.offlineCache.Add(*data) + } + } +} diff --git a/agent/agent_offline_cache.go b/agent/agent_offline_cache.go new file mode 100644 index 000000000..ff478bb33 --- /dev/null +++ b/agent/agent_offline_cache.go @@ -0,0 +1,37 @@ +package agent + +import ( + "sync" + + "github.com/henrygd/beszel/internal/entities/system" +) + +type systemOfflineCache struct { + sync.Mutex + cache []*system.CombinedData +} + +func NewSystemOfflineCache() *systemOfflineCache { + return &systemOfflineCache{ + cache: make([]*system.CombinedData, 0), + } +} + +// GetAll retrieves all cached combined data and clears the cache. +func (c *systemOfflineCache) GetAll() (result []*system.CombinedData) { + c.Lock() + defer c.Unlock() + + result = append(result, c.cache...) + c.cache = c.cache[:0] + + return result +} + +// Add appends a new combined data snapshot to the cache. +func (c *systemOfflineCache) Add(data system.CombinedData) { + c.Lock() + defer c.Unlock() + + c.cache = append(c.cache, &data) +} diff --git a/agent/client.go b/agent/client.go index 48a965b93..09721d208 100644 --- a/agent/client.go +++ b/agent/client.go @@ -269,7 +269,7 @@ func (client *WebSocketClient) sendResponse(data any, requestID *uint32) error { // Set the appropriate typed field based on data type switch v := data.(type) { - case *system.CombinedData: + case []*system.CombinedData: response.SystemData = v case *common.FingerprintResponse: response.Fingerprint = v diff --git a/agent/handlers.go b/agent/handlers.go index 931c4dfe1..5b4720d18 100644 --- a/agent/handlers.go +++ b/agent/handlers.go @@ -94,8 +94,10 @@ func (h *GetDataHandler) Handle(hctx *HandlerContext) error { var options common.DataRequestOptions _ = cbor.Unmarshal(hctx.Request.Data, &options) - sysStats := hctx.Agent.gatherStats(options.CacheTimeMs) - return hctx.SendResponse(sysStats, hctx.RequestID) + data := hctx.Agent.offlineCache.GetAll() + data = append(data, hctx.Agent.gatherStats(options.CacheTimeMs)) + + return hctx.SendResponse(data, hctx.RequestID) } //////////////////////////////////////////////////////////////////////////// diff --git a/agent/server.go b/agent/server.go index c826d67f2..c20fa8185 100644 --- a/agent/server.go +++ b/agent/server.go @@ -168,7 +168,7 @@ func (a *Agent) handleSSHRequest(w io.Writer, req *common.HubRequest[cbor.RawMes sshResponder := func(data any, requestID *uint32) error { response := common.AgentResponse{Id: requestID} switch v := data.(type) { - case *system.CombinedData: + case []*system.CombinedData: response.SystemData = v case string: response.String = &v diff --git a/internal/alerts/alerts_system.go b/internal/alerts/alerts_system.go index c082178bc..0c5445d3c 100644 --- a/internal/alerts/alerts_system.go +++ b/internal/alerts/alerts_system.go @@ -108,29 +108,29 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst } systemStats := []struct { - Stats []byte `db:"stats"` - Created types.DateTime `db:"created"` + Stats []byte `db:"stats"` + Timestamp types.DateTime `db:"timestamp"` }{} err = am.hub.DB(). - Select("stats", "created"). + Select("stats", "timestamp"). From("system_stats"). Where(dbx.NewExp( - "system={:system} AND type='1m' AND created > {:created}", + "system={:system} AND type='1m' AND timestamp > {:timestamp}", dbx.Params{ "system": systemRecord.Id, // subtract some time to give us a bit of buffer - "created": oldestTime.Add(-time.Second * 90), + "timestamp": oldestTime.Add(-time.Second * 90), }, )). - OrderBy("created"). + OrderBy("timestamp"). All(&systemStats) if err != nil || len(systemStats) == 0 { return err } // get oldest record creation time from first record in the slice - oldestRecordTime := systemStats[0].Created.Time() + oldestRecordTime := systemStats[0].Timestamp.Time() // log.Println("oldestRecordTime", oldestRecordTime.String()) // Filter validAlerts to keep only those with time newer than oldestRecord @@ -153,7 +153,7 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst for i := range systemStats { stat := systemStats[i] // subtract 10 seconds to give a small time buffer - systemStatsCreation := stat.Created.Time().Add(-time.Second * 10) + systemStatsCreation := stat.Timestamp.Time().Add(-time.Second * 10) if err := json.Unmarshal(stat.Stats, &stats); err != nil { return err } diff --git a/internal/common/common-ws.go b/internal/common/common-ws.go index 290e0dbb6..1a1a8f040 100644 --- a/internal/common/common-ws.go +++ b/internal/common/common-ws.go @@ -34,7 +34,7 @@ type HubRequest[T any] struct { // AgentResponse defines the structure for responses sent from agent to hub. type AgentResponse struct { Id *uint32 `cbor:"0,keyasint,omitempty"` - SystemData *system.CombinedData `cbor:"1,keyasint,omitempty,omitzero"` + SystemData []*system.CombinedData `cbor:"1,keyasint,omitempty,omitzero"` Fingerprint *FingerprintResponse `cbor:"2,keyasint,omitempty,omitzero"` Error string `cbor:"3,keyasint,omitempty,omitzero"` String *string `cbor:"4,keyasint,omitempty,omitzero"` diff --git a/internal/entities/system/system.go b/internal/entities/system/system.go index 544fa13b3..078dfc30f 100644 --- a/internal/entities/system/system.go +++ b/internal/entities/system/system.go @@ -156,4 +156,5 @@ type CombinedData struct { Info Info `json:"info" cbor:"1,keyasint"` Containers []*container.Stats `json:"container" cbor:"2,keyasint"` SystemdServices []*systemd.Service `json:"systemd,omitempty" cbor:"3,keyasint,omitempty"` + Timestamp *time.Time `json:"ts,omitempty" cbor:"4,keyasint,omitempty"` } diff --git a/internal/hub/systems/system.go b/internal/hub/systems/system.go index f18704aa6..1dcf21803 100644 --- a/internal/hub/systems/system.go +++ b/internal/hub/systems/system.go @@ -29,25 +29,24 @@ import ( ) type System struct { - Id string `db:"id"` - Host string `db:"host"` - Port string `db:"port"` - Status string `db:"status"` - manager *SystemManager // Manager that this system belongs to - client *ssh.Client // SSH client for fetching data - data *system.CombinedData // system data from agent - ctx context.Context // Context for stopping the updater - cancel context.CancelFunc // Stops and removes system from updater - WsConn *ws.WsConn // Handler for agent WebSocket connection - agentVersion semver.Version // Agent version - updateTicker *time.Ticker // Ticker for updating the system - smartOnce sync.Once // Once for fetching and saving smart devices + Id string `db:"id"` + Host string `db:"host"` + Port string `db:"port"` + Status string `db:"status"` + manager *SystemManager // Manager that this system belongs to + client *ssh.Client // SSH client for fetching data + data []*system.CombinedData // system data from agent + ctx context.Context // Context for stopping the updater + cancel context.CancelFunc // Stops and removes system from updater + WsConn *ws.WsConn // Handler for agent WebSocket connection + agentVersion semver.Version // Agent version + updateTicker *time.Ticker // Ticker for updating the system + smartOnce sync.Once // Once for fetching and saving smart devices } func (sm *SystemManager) NewSystem(systemId string) *System { system := &System{ - Id: systemId, - data: &system.CombinedData{}, + Id: systemId, } system.ctx, system.cancel = system.getContext() return system @@ -135,58 +134,66 @@ func (sys *System) handlePaused() { } // createRecords updates the system record and adds system_stats and container_stats records -func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error) { +func (sys *System) createRecords(data []*system.CombinedData) (*core.Record, error) { + if len(data) == 0 { + return nil, errors.New("no data to create records") + } + systemRecord, err := sys.getRecord() if err != nil { return nil, err } hub := sys.manager.hub - err = hub.RunInTransaction(func(txApp core.App) error { - // add system_stats and container_stats records - systemStatsCollection, err := txApp.FindCachedCollectionByNameOrId("system_stats") - if err != nil { - return err - } - systemStatsRecord := core.NewRecord(systemStatsCollection) - systemStatsRecord.Set("system", systemRecord.Id) - systemStatsRecord.Set("stats", data.Stats) - systemStatsRecord.Set("type", "1m") - if err := txApp.SaveNoValidate(systemStatsRecord); err != nil { - return err - } - if len(data.Containers) > 0 { - // add / update containers records - if data.Containers[0].Id != "" { - if err := createContainerRecords(txApp, data.Containers, sys.Id); err != nil { - return err - } - } - // add new container_stats record - containerStatsCollection, err := txApp.FindCachedCollectionByNameOrId("container_stats") + err = hub.RunInTransaction(func(txApp core.App) error { + for _, d := range data { // add system_stats and container_stats records + systemStatsCollection, err := txApp.FindCachedCollectionByNameOrId("system_stats") if err != nil { return err } - containerStatsRecord := core.NewRecord(containerStatsCollection) - containerStatsRecord.Set("system", systemRecord.Id) - containerStatsRecord.Set("stats", data.Containers) - containerStatsRecord.Set("type", "1m") - if err := txApp.SaveNoValidate(containerStatsRecord); err != nil { + + systemStatsRecord := core.NewRecord(systemStatsCollection) + systemStatsRecord.Set("timestamp", d.Timestamp) + systemStatsRecord.Set("system", systemRecord.Id) + systemStatsRecord.Set("stats", d.Stats) + systemStatsRecord.Set("type", "1m") + if err := txApp.SaveNoValidate(systemStatsRecord); err != nil { return err } - } + if len(d.Containers) > 0 { + // add / update containers records + if d.Containers[0].Id != "" { + if err := createContainerRecords(txApp, d.Containers, sys.Id); err != nil { + return err + } + } + // add new container_stats record + containerStatsCollection, err := txApp.FindCachedCollectionByNameOrId("container_stats") + if err != nil { + return err + } + containerStatsRecord := core.NewRecord(containerStatsCollection) + containerStatsRecord.Set("timestamp", d.Timestamp) + containerStatsRecord.Set("system", systemRecord.Id) + containerStatsRecord.Set("stats", d.Containers) + containerStatsRecord.Set("type", "1m") + if err := txApp.SaveNoValidate(containerStatsRecord); err != nil { + return err + } + } - // add new systemd_stats record - if len(data.SystemdServices) > 0 { - if err := createSystemdStatsRecords(txApp, data.SystemdServices, sys.Id); err != nil { - return err + // add new systemd_stats record + if len(d.SystemdServices) > 0 { + if err := createSystemdStatsRecords(txApp, d.SystemdServices, sys.Id); err != nil { + return err + } } } // update system record (do this last because it triggers alerts and we need above records to be inserted first) systemRecord.Set("status", up) - systemRecord.Set("info", data.Info) + systemRecord.Set("info", data[0].Info) if err := txApp.SaveNoValidate(systemRecord); err != nil { return err } @@ -303,11 +310,7 @@ func (sys *System) getContext() (context.Context, context.CancelFunc) { // fetchDataFromAgent attempts to fetch data from the agent, // prioritizing WebSocket if available. -func (sys *System) fetchDataFromAgent(options common.DataRequestOptions) (*system.CombinedData, error) { - if sys.data == nil { - sys.data = &system.CombinedData{} - } - +func (sys *System) fetchDataFromAgent(options common.DataRequestOptions) ([]*system.CombinedData, error) { if sys.WsConn != nil && sys.WsConn.IsConnected() { wsData, err := sys.fetchDataViaWebSocket(options) if err == nil { @@ -324,11 +327,11 @@ func (sys *System) fetchDataFromAgent(options common.DataRequestOptions) (*syste return sshData, nil } -func (sys *System) fetchDataViaWebSocket(options common.DataRequestOptions) (*system.CombinedData, error) { +func (sys *System) fetchDataViaWebSocket(options common.DataRequestOptions) ([]*system.CombinedData, error) { if sys.WsConn == nil || !sys.WsConn.IsConnected() { return nil, errors.New("no websocket connection") } - err := sys.WsConn.RequestSystemData(context.Background(), sys.data, options) + err := sys.WsConn.RequestSystemData(context.Background(), &sys.data, options) if err != nil { return nil, err } @@ -448,7 +451,7 @@ func makeStableHashId(strings ...string) string { // fetchDataViaSSH handles fetching data using SSH. // This function encapsulates the original SSH logic. // It updates sys.data directly upon successful fetch. -func (sys *System) fetchDataViaSSH(options common.DataRequestOptions) (*system.CombinedData, error) { +func (sys *System) fetchDataViaSSH(options common.DataRequestOptions) ([]*system.CombinedData, error) { err := sys.runSSHOperation(4*time.Second, 1, func(session *ssh.Session) (bool, error) { stdout, err := session.StdoutPipe() if err != nil { @@ -459,8 +462,7 @@ func (sys *System) fetchDataViaSSH(options common.DataRequestOptions) (*system.C return false, err } - *sys.data = system.CombinedData{} - + sys.data = sys.data[:0] if sys.agentVersion.GTE(beszel.MinVersionAgentResponse) && stdinErr == nil { req := common.HubRequest[any]{Action: common.GetData, Data: options} _ = cbor.NewEncoder(stdin).Encode(req) @@ -468,7 +470,9 @@ func (sys *System) fetchDataViaSSH(options common.DataRequestOptions) (*system.C var resp common.AgentResponse if decErr := cbor.NewDecoder(stdout).Decode(&resp); decErr == nil && resp.SystemData != nil { - *sys.data = *resp.SystemData + if resp.SystemData != nil { + sys.data = append(sys.data, resp.SystemData...) + } if err := session.Wait(); err != nil { return false, err } diff --git a/internal/hub/systems/system_manager.go b/internal/hub/systems/system_manager.go index 9dbe4b14f..6afff5bba 100644 --- a/internal/hub/systems/system_manager.go +++ b/internal/hub/systems/system_manager.go @@ -208,7 +208,8 @@ func (sm *SystemManager) onRecordAfterUpdateSuccess(e *core.RecordEvent) error { // Trigger system alerts when system comes online if newStatus == up { - if err := sm.hub.HandleSystemAlerts(e.Record, system.data); err != nil { + // TODO: update this to handle multiple data records + if err := sm.hub.HandleSystemAlerts(e.Record, system.data[0]); err != nil { e.App.Logger().Error("Error handling system alerts", "err", err) } } @@ -243,7 +244,6 @@ func (sm *SystemManager) AddSystem(sys *System) error { // Initialize system for monitoring sys.manager = sm sys.ctx, sys.cancel = sys.getContext() - sys.data = &system.CombinedData{} sm.systems.Set(sys.Id, sys) // Start monitoring in background diff --git a/internal/hub/systems/systems_test_helpers.go b/internal/hub/systems/systems_test_helpers.go index b49d8369e..f65fa1470 100644 --- a/internal/hub/systems/systems_test_helpers.go +++ b/internal/hub/systems/systems_test_helpers.go @@ -61,7 +61,7 @@ func (sm *SystemManager) GetAllSystemIDs() []string { // TESTING ONLY: GetSystemData returns the combined data for a system with the given ID // Returns nil if the system doesn't exist // This method is intended for testing -func (sm *SystemManager) GetSystemData(systemID string) *entities.CombinedData { +func (sm *SystemManager) GetSystemData(systemID string) []*entities.CombinedData { sys, ok := sm.systems.GetOk(systemID) if !ok { return nil diff --git a/internal/hub/ws/handlers.go b/internal/hub/ws/handlers.go index d94722797..5f04cee32 100644 --- a/internal/hub/ws/handlers.go +++ b/internal/hub/ws/handlers.go @@ -32,7 +32,7 @@ func (h *BaseHandler) HandleLegacy(rawData []byte) error { // systemDataHandler implements ResponseHandler for system data requests type systemDataHandler struct { - data *system.CombinedData + data *[]*system.CombinedData } func (h *systemDataHandler) HandleLegacy(rawData []byte) error { @@ -40,14 +40,12 @@ func (h *systemDataHandler) HandleLegacy(rawData []byte) error { } func (h *systemDataHandler) Handle(agentResponse common.AgentResponse) error { - if agentResponse.SystemData != nil { - *h.data = *agentResponse.SystemData - } + *h.data = append(*h.data, agentResponse.SystemData...) return nil } // RequestSystemData requests system metrics from the agent and unmarshals the response. -func (ws *WsConn) RequestSystemData(ctx context.Context, data *system.CombinedData, options common.DataRequestOptions) error { +func (ws *WsConn) RequestSystemData(ctx context.Context, data *[]*system.CombinedData, options common.DataRequestOptions) error { if !ws.IsConnected() { return gws.ErrConnClosed } diff --git a/internal/migrations/1761659006_add_time.go b/internal/migrations/1761659006_add_time.go new file mode 100644 index 000000000..2fb6d84ae --- /dev/null +++ b/internal/migrations/1761659006_add_time.go @@ -0,0 +1,69 @@ +package migrations + +import ( + "fmt" + + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + var collectionNames = []string{"system_stats", "container_stats"} + + m.Register(func(app core.App) error { + return app.RunInTransaction(func(txApp core.App) error { + updateCollection := func(collectionName string) error { + fmt.Println("Updating collection:", collectionName) + collection, err := txApp.FindCollectionByNameOrId(collectionName) + if err != nil { + return err + } + + collection.Fields.Add(&core.DateField{ + Name: "timestamp", + Required: true, + }) + + err = txApp.Save(collection) + if err != nil { + return err + } + + query := fmt.Sprintf("UPDATE %s SET timestamp = created", collectionName) + fmt.Println("Running query:", query) + _, err = txApp.DB().NewQuery(query).Execute() + return err + } + + for _, collectionName := range collectionNames { + err := updateCollection(collectionName) + if err != nil { + return err + } + } + + return nil + }) + }, func(app core.App) error { + return app.RunInTransaction(func(txApp core.App) error { + revertCollection := func(collectionName string) error { + fmt.Println("Reverting collection:", collectionName) + collection, err := txApp.FindCollectionByNameOrId(collectionName) + if err != nil { + return err + } + collection.Fields.RemoveByName("timestamp") + return txApp.Save(collection) + } + + for _, collectionName := range collectionNames { + err := revertCollection(collectionName) + if err != nil { + return err + } + } + + return nil + }) + }) +} diff --git a/internal/records/records.go b/internal/records/records.go index eaf4ef7f9..6f7a543f0 100644 --- a/internal/records/records.go +++ b/internal/records/records.go @@ -113,8 +113,8 @@ func (rm *RecordManager) CreateLongerRecords() { count, err := txApp.CountRecords( collection.Id, dbx.NewExp( - "system = {:system} AND type = {:type} AND created > {:created}", - dbx.Params{"type": recordData.longerType, "system": system.Id, "created": longerRecordPeriod}, + "system = {:system} AND type = {:type} AND timestamp > {:timestamp}", + dbx.Params{"type": recordData.longerType, "system": system.Id, "timestamp": longerRecordPeriod}, ), ) // continue if longer record exists @@ -129,11 +129,11 @@ func (rm *RecordManager) CreateLongerRecords() { Select("id"). From(collection.Name). AndWhere(dbx.NewExp( - "system={:system} AND type={:type} AND created > {:created}", + "system={:system} AND type={:type} AND timestamp > {:timestamp}", dbx.Params{ - "type": recordData.shorterType, - "system": system.Id, - "created": shorterRecordPeriod, + "type": recordData.shorterType, + "system": system.Id, + "timestamp": shorterRecordPeriod, }, )). All(&recordIds) @@ -144,13 +144,13 @@ func (rm *RecordManager) CreateLongerRecords() { } // average the shorter records and create longer record longerRecord := core.NewRecord(collection) + longerRecord.Set("timestamp", time.Now().UTC()) longerRecord.Set("system", system.Id) longerRecord.Set("type", recordData.longerType) switch collection.Name { case "system_stats": longerRecord.Set("stats", rm.AverageSystemStats(db, recordIds)) case "container_stats": - longerRecord.Set("stats", rm.AverageContainerStats(db, recordIds)) } if err := txApp.SaveNoValidate(longerRecord); err != nil { diff --git a/internal/site/src/components/alerts-history-columns.tsx b/internal/site/src/components/alerts-history-columns.tsx index b3162e239..6bbebdfd3 100644 --- a/internal/site/src/components/alerts-history-columns.tsx +++ b/internal/site/src/components/alerts-history-columns.tsx @@ -100,8 +100,8 @@ export const alertsHistoryColumns: ColumnDef[] = [ }, }, { - accessorKey: "created", - accessorFn: (record) => formatShortDate(record.created), + accessorKey: "timestamp", + accessorFn: (record) => formatShortDate(record.timestamp), enableSorting: true, invertSorting: true, header: ({ column }) => ( @@ -110,7 +110,7 @@ export const alertsHistoryColumns: ColumnDef[] = [ ), cell: ({ getValue, row }) => ( - + {getValue() as string} ), @@ -141,12 +141,12 @@ export const alertsHistoryColumns: ColumnDef[] = [ invertSorting: true, enableSorting: true, sortingFn: (rowA, rowB) => { - const aCreated = new Date(rowA.original.created) - const bCreated = new Date(rowB.original.created) + const aTimestamp = new Date(rowA.original.timestamp) + const bTimestamp = new Date(rowB.original.timestamp) const aResolved = rowA.original.resolved ? new Date(rowA.original.resolved) : null const bResolved = rowB.original.resolved ? new Date(rowB.original.resolved) : null - const aDuration = aResolved ? aResolved.getTime() - aCreated.getTime() : null - const bDuration = bResolved ? bResolved.getTime() - bCreated.getTime() : null + const aDuration = aResolved ? aResolved.getTime() - aTimestamp.getTime() : null + const bDuration = bResolved ? bResolved.getTime() - bTimestamp.getTime() : null if (!aDuration && bDuration) return -1 if (aDuration && !bDuration) return 1 return (aDuration || 0) - (bDuration || 0) @@ -157,7 +157,7 @@ export const alertsHistoryColumns: ColumnDef[] = [ ), cell: ({ row }) => { - const duration = formatDuration(row.original.created, row.original.resolved) + const duration = formatDuration(row.original.timestamp, row.original.resolved) if (!duration) { return null } diff --git a/internal/site/src/components/charts/area-chart.tsx b/internal/site/src/components/charts/area-chart.tsx index 4838402bb..bbd0c45df 100644 --- a/internal/site/src/components/charts/area-chart.tsx +++ b/internal/site/src/components/charts/area-chart.tsx @@ -95,7 +95,7 @@ export default function AreaChartDefault({ itemSorter={itemSorter} content={ formatShortDate(data[0].payload.created)} + labelFormatter={(_, data) => formatShortDate(data[0].payload.timestamp)} contentFormatter={contentFormatter} showTotal={showTotal} /> diff --git a/internal/site/src/components/charts/container-chart.tsx b/internal/site/src/components/charts/container-chart.tsx index f3b99271f..db48fc396 100644 --- a/internal/site/src/components/charts/container-chart.tsx +++ b/internal/site/src/components/charts/container-chart.tsx @@ -137,7 +137,7 @@ export default memo(function ContainerChart({ animationEasing="ease-out" animationDuration={150} truncate={true} - labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} + labelFormatter={(_, data) => formatShortDate(data[0].payload.timestamp)} // @ts-expect-error itemSorter={(a, b) => b.value - a.value} content={} diff --git a/internal/site/src/components/charts/disk-chart.tsx b/internal/site/src/components/charts/disk-chart.tsx index 4be78f93f..30dca533d 100644 --- a/internal/site/src/components/charts/disk-chart.tsx +++ b/internal/site/src/components/charts/disk-chart.tsx @@ -58,7 +58,7 @@ export default memo(function DiskChart({ animationDuration={150} content={ formatShortDate(data[0].payload.created)} + labelFormatter={(_, data) => formatShortDate(data[0].payload.timestamp)} contentFormatter={({ value }) => { const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true) return decimalString(convertedValue) + " " + unit diff --git a/internal/site/src/components/charts/gpu-power-chart.tsx b/internal/site/src/components/charts/gpu-power-chart.tsx index c09287e24..a61a5445c 100644 --- a/internal/site/src/components/charts/gpu-power-chart.tsx +++ b/internal/site/src/components/charts/gpu-power-chart.tsx @@ -28,7 +28,7 @@ export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData for (const stats of chartData.systemStats) { const gpus = stats.stats?.g ?? {} - const data = { created: stats.created } as Record + const data = { timestamp: stats.timestamp } as Record for (const id in gpus) { const gpu = gpus[id] as GPUData data[gpu.n] = gpu @@ -91,7 +91,7 @@ export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData itemSorter={(a, b) => b.value - a.value} content={ formatShortDate(data[0].payload.created)} + labelFormatter={(_, data) => formatShortDate(data[0].payload.timestamp)} contentFormatter={(item) => `${decimalString(item.value)}W`} // indicator="line" /> diff --git a/internal/site/src/components/charts/line-chart.tsx b/internal/site/src/components/charts/line-chart.tsx index 45fcc996e..6bb5121fd 100644 --- a/internal/site/src/components/charts/line-chart.tsx +++ b/internal/site/src/components/charts/line-chart.tsx @@ -78,7 +78,7 @@ export default function LineChartDefault({ itemSorter={itemSorter} content={ formatShortDate(data[0].payload.created)} + labelFormatter={(_, data) => formatShortDate(data[0].payload.timestamp)} contentFormatter={contentFormatter} /> } diff --git a/internal/site/src/components/charts/load-average-chart.tsx b/internal/site/src/components/charts/load-average-chart.tsx index c87636386..665567e6e 100644 --- a/internal/site/src/components/charts/load-average-chart.tsx +++ b/internal/site/src/components/charts/load-average-chart.tsx @@ -61,7 +61,7 @@ export default memo(function LoadAverageChart({ chartData }: { chartData: ChartD animationDuration={150} content={ formatShortDate(data[0].payload.created)} + labelFormatter={(_, data) => formatShortDate(data[0].payload.timestamp)} contentFormatter={(item) => decimalString(item.value)} /> } diff --git a/internal/site/src/components/charts/mem-chart.tsx b/internal/site/src/components/charts/mem-chart.tsx index 98dd05a3e..85ba0f6e7 100644 --- a/internal/site/src/components/charts/mem-chart.tsx +++ b/internal/site/src/components/charts/mem-chart.tsx @@ -55,7 +55,7 @@ export default memo(function MemChart({ chartData, showMax }: { chartData: Chart a.order - b.order} - labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} + labelFormatter={(_, data) => formatShortDate(data[0].payload.timestamp)} contentFormatter={({ value }) => { // mem values are supplied as GB const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true) diff --git a/internal/site/src/components/charts/swap-chart.tsx b/internal/site/src/components/charts/swap-chart.tsx index a45c1e9e6..85c76052c 100644 --- a/internal/site/src/components/charts/swap-chart.tsx +++ b/internal/site/src/components/charts/swap-chart.tsx @@ -44,7 +44,7 @@ export default memo(function SwapChart({ chartData }: { chartData: ChartData }) animationDuration={150} content={ formatShortDate(data[0].payload.created)} + labelFormatter={(_, data) => formatShortDate(data[0].payload.timestamp)} contentFormatter={({ value }) => { // mem values are supplied as GB const { value: convertedValue, unit } = formatBytes(value * 1024, false, userSettings.unitDisk, true) diff --git a/internal/site/src/components/charts/temperature-chart.tsx b/internal/site/src/components/charts/temperature-chart.tsx index 60267d974..39d025744 100644 --- a/internal/site/src/components/charts/temperature-chart.tsx +++ b/internal/site/src/components/charts/temperature-chart.tsx @@ -31,7 +31,7 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD } const tempSums = {} as Record for (const data of chartData.systemStats) { - const newData = { created: data.created } as Record + const newData = { timestamp: data.timestamp } as Record const keys = Object.keys(data.stats?.t ?? {}) for (let i = 0; i < keys.length; i++) { const key = keys[i] @@ -81,7 +81,7 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD itemSorter={(a, b) => b.value - a.value} content={ formatShortDate(data[0].payload.created)} + labelFormatter={(_, data) => formatShortDate(data[0].payload.timestamp)} contentFormatter={(item) => { const { value, unit } = formatTemperature(item.value, userSettings.unitTemp) return decimalString(value) + " " + unit diff --git a/internal/site/src/components/routes/settings/alerts-history-data-table.tsx b/internal/site/src/components/routes/settings/alerts-history-data-table.tsx index 84758ba2d..f262f12e7 100644 --- a/internal/site/src/components/routes/settings/alerts-history-data-table.tsx +++ b/internal/site/src/components/routes/settings/alerts-history-data-table.tsx @@ -78,13 +78,13 @@ export default function AlertsHistoryDataTable() { let unsubscribe: (() => void) | undefined const pbOptions = { expand: "system", - fields: "id,name,value,state,created,resolved,expand.system.name", + fields: "id,name,value,state,timestamp,resolved,expand.system.name", } // Initial load pb.collection("alerts_history") .getList(0, 200, { ...pbOptions, - sort: "-created", + sort: "-timestamp", }) .then(({ items }) => setData(items)) @@ -156,12 +156,12 @@ export default function AlertsHistoryDataTable() { globalFilterFn: (row, _columnId, filterValue) => { const system = row.original.expand?.system?.name ?? "" const name = row.getValue("name") ?? "" - const created = row.getValue("created") ?? "" + const timestamp = row.getValue("timestamp") ?? "" const search = String(filterValue).toLowerCase() return ( system.toLowerCase().includes(search) || (name as string).toLowerCase().includes(search) || - (created as string).toLowerCase().includes(search) + (timestamp as string).toLowerCase().includes(search) ) }, }) @@ -202,9 +202,9 @@ export default function AlertsHistoryDataTable() { name: (record) => alertInfo[record.name]?.name() || record.name, value: (record) => record.value + (alertInfo[record.name]?.unit ?? ""), state: (record) => (record.resolved ? t`Resolved` : t`Active`), - created: (record) => formatShortDate(record.created), + timestamp: (record) => formatShortDate(record.timestamp), resolved: (record) => (record.resolved ? formatShortDate(record.resolved) : ""), - duration: (record) => (record.resolved ? formatDuration(record.created, record.resolved) : ""), + duration: (record) => (record.resolved ? formatDuration(record.timestamp, record.resolved) : ""), } const csvRows = [Object.keys(cells).join(",")] for (const row of selectedRows) { diff --git a/internal/site/src/components/routes/system.tsx b/internal/site/src/components/routes/system.tsx index 8feec0200..30ad94487 100644 --- a/internal/site/src/components/routes/system.tsx +++ b/internal/site/src/components/routes/system.tsx @@ -88,10 +88,10 @@ type ChartTimeData = { const cache = new Map() // create ticks and domain for charts -function getTimeData(chartTime: ChartTimes, lastCreated: number) { +function getTimeData(chartTime: ChartTimes, lastTimestamp: number) { const cached = cache.get("td") as ChartTimeData | undefined if (cached && cached.chartTime === chartTime) { - if (!lastCreated || cached.time >= lastCreated) { + if (!lastTimestamp || cached.time >= lastTimestamp) { return cached.data } } @@ -109,27 +109,27 @@ function getTimeData(chartTime: ChartTimes, lastCreated: number) { } // add empty values between records to make gaps if interval is too large -function addEmptyValues( +function addEmptyValues( prevRecords: T[], newRecords: T[], expectedInterval: number ): T[] { const modifiedRecords: T[] = [] - let prevTime = (prevRecords.at(-1)?.created ?? 0) as number + let prevTime = (prevRecords.at(-1)?.timestamp ?? 0) as number for (let i = 0; i < newRecords.length; i++) { const record = newRecords[i] - if (record.created !== null) { - record.created = new Date(record.created).getTime() + if (record.timestamp !== null) { + record.timestamp = new Date(record.timestamp).getTime() } - if (prevTime && record.created !== null) { - const interval = record.created - prevTime + if (prevTime && record.timestamp !== null) { + const interval = record.timestamp - prevTime // if interval is too large, add a null record if (interval > expectedInterval / 2 + expectedInterval) { - modifiedRecords.push({ created: null, ...("stats" in record ? { stats: null } : {}) } as T) + modifiedRecords.push({ timestamp: null, ...("stats" in record ? { stats: null } : {}) } as T) } } - if (record.created !== null) { - prevTime = record.created + if (record.timestamp !== null) { + prevTime = record.timestamp } modifiedRecords.push(record) } @@ -142,15 +142,15 @@ async function getStats( chartTime: ChartTimes ): Promise { const cachedStats = cache.get(`${system.id}_${chartTime}_${collection}`) as T[] | undefined - const lastCached = cachedStats?.at(-1)?.created as number + const lastCached = cachedStats?.at(-1)?.timestamp as number return await pb.collection(collection).getFullList({ - filter: pb.filter("system={:id} && created > {:created} && type={:type}", { + filter: pb.filter("system={:id} && timestamp > {:timestamp} && type={:type}", { id: system.id, - created: getPbTimestamp(chartTime, lastCached ? new Date(lastCached + 1000) : undefined), + timestamp: getPbTimestamp(chartTime, lastCached ? new Date(lastCached + 1000) : undefined), type: chartTimeData[chartTime].type, }), - fields: "created,stats", - sort: "created", + fields: "timestamp,stats", + sort: "timestamp", }) } @@ -231,14 +231,14 @@ export default memo(function SystemDetail({ id }: { id: string }) { (data: { container: ContainerStatsRecord[]; info: SystemInfo; stats: SystemStats }) => { if (data.container?.length > 0) { const newContainerData = makeContainerData([ - { created: Date.now(), stats: data.container } as unknown as ContainerStatsRecord, + { timestamp: Date.now(), stats: data.container } as unknown as ContainerStatsRecord, ]) setContainerData((prevData) => addEmptyValues(prevData, prevData.slice(-59).concat(newContainerData), 1000)) } setSystemStats((prevStats) => addEmptyValues( prevStats, - prevStats.slice(-59).concat({ created: Date.now(), stats: data.stats } as SystemStatsRecord), + prevStats.slice(-59).concat({ timestamp: Date.now(), stats: data.stats } as SystemStatsRecord), 1000 ) ) @@ -255,16 +255,16 @@ export default memo(function SystemDetail({ id }: { id: string }) { // biome-ignore lint/correctness/useExhaustiveDependencies: not necessary const chartData: ChartData = useMemo(() => { - const lastCreated = Math.max( - (systemStats.at(-1)?.created as number) ?? 0, - (containerData.at(-1)?.created as number) ?? 0 + const lastTimestamp = Math.max( + (systemStats.at(-1)?.timestamp as number) ?? 0, + (containerData.at(-1)?.timestamp as number) ?? 0 ) return { systemStats, containerData, chartTime, orientation: direction === "rtl" ? "right" : "left", - ...getTimeData(chartTime, lastCreated), + ...getTimeData(chartTime, lastTimestamp), agentVersion: parseSemVer(system?.info?.v), } }, [systemStats, containerData, direction]) @@ -275,15 +275,15 @@ export default memo(function SystemDetail({ id }: { id: string }) { // make container stats for charts const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => { const containerData = [] as ChartData["containerData"] - for (let { created, stats } of containers) { - if (!created) { + for (let { timestamp, stats } of containers) { + if (!timestamp) { // @ts-expect-error add null value for gaps - containerData.push({ created: null }) + containerData.push({ timestamp: null }) continue } - created = new Date(created).getTime() + timestamp = new Date(timestamp).getTime() // @ts-expect-error not dealing with this rn - const containerStats: ChartData["containerData"][0] = { created } + const containerStats: ChartData["containerData"][0] = { timestamp } for (const container of stats) { containerStats[container.n] = container } diff --git a/internal/site/src/components/ui/chart.tsx b/internal/site/src/components/ui/chart.tsx index f0ac885a9..231235870 100644 --- a/internal/site/src/components/ui/chart.tsx +++ b/internal/site/src/components/ui/chart.tsx @@ -431,7 +431,7 @@ const xAxis = ({ domain, ticks, chartTime }: ChartData) => { } cachedAxis = ( ( /** Calculate duration between two dates and format as human-readable string */ export function formatDuration( - createdDate: string | null | undefined, + timestampDate: string | null | undefined, resolvedDate: string | null | undefined ): string { - const created = createdDate ? new Date(createdDate) : null + const timestamp = timestampDate ? new Date(timestampDate) : null const resolved = resolvedDate ? new Date(resolvedDate) : null - if (!created || !resolved) return "" + if (!timestamp || !resolved) return "" - const diffMs = resolved.getTime() - created.getTime() + const diffMs = resolved.getTime() - timestamp.getTime() if (diffMs < 0) return "" const totalSeconds = Math.floor(diffMs / 1000) diff --git a/internal/site/src/types.d.ts b/internal/site/src/types.d.ts index df6ac03e3..aed121217 100644 --- a/internal/site/src/types.d.ts +++ b/internal/site/src/types.d.ts @@ -202,7 +202,7 @@ export interface ExtraFsStats { export interface ContainerStatsRecord extends RecordModel { system: string stats: ContainerStats[] - created: string | number + timestamp: string | number } interface ContainerStats { @@ -221,7 +221,7 @@ interface ContainerStats { export interface SystemStatsRecord extends RecordModel { system: string stats: SystemStats - created: string | number + timestamp: string | number } export interface AlertRecord extends RecordModel { @@ -240,7 +240,7 @@ export interface AlertsHistoryRecord extends RecordModel { system: string name: string val: number - created: string + timestamp: string resolved?: string | null } @@ -299,9 +299,9 @@ export interface UserSettings { } type ChartDataContainer = { - created: number | null + timestamp: number | null } & { - [key: string]: key extends "created" ? never : ContainerStats + [key: string]: key extends "timestamp" ? never : ContainerStats } export interface SemVer {