diff --git a/main.go b/main.go index a6b88df..068826b 100644 --- a/main.go +++ b/main.go @@ -83,12 +83,10 @@ func main() { }) e.Router.GET("/icons/*", apis.StaticDirectoryHandler(os.DirFS("./site/public/icons"), false)) e.Router.Any("/*", echo.WrapHandler(proxy)) - e.Router.Any("/", echo.WrapHandler(proxy)) + // e.Router.Any("/", echo.WrapHandler(proxy)) default: - assets, _ := site.Assets() - icons, _ := site.Icons() - e.Router.GET("/icons/*", apis.StaticDirectoryHandler(icons, false)) - e.Router.Any("/*", apis.StaticDirectoryHandler(assets, true)) + e.Router.GET("/icons/*", apis.StaticDirectoryHandler(site.Icons, false)) + e.Router.Any("/*", apis.StaticDirectoryHandler(site.Dist, true)) } return nil }) @@ -186,8 +184,7 @@ func main() { }) app.OnModelAfterCreate("system_stats").Add(func(e *core.ModelEvent) error { - createLongerRecords(e.Model.(*models.Record)) - // createLongerRecords(e.Model.(*models.Record).OriginalCopy(), e.Model.(*models.Record)) + createLongerRecords("system_stats", e.Model.(*models.Record)) return nil }) diff --git a/readme.md b/readme.md index eca7776..1e8eab7 100644 --- a/readme.md +++ b/readme.md @@ -97,11 +97,11 @@ Because Beszel is built on top of PocketBase, you can use the normal PocketBase ## Security -The hub and agent communicate over SSH, so they do not need to be exposed to the internet. +The hub and agent communicate over SSH, so they don't need to be exposed to the internet. And the connection won't break if you put your own auth gateway, such as Authelia, in front of the hub. When the hub is started for the first time, it generates an ED25519 key pair. -The agent's SSH server is configured to accept connections only using this key. It also does not provide a pty or accept any input, so it is not possible to execute commands on the agent. +The agent's SSH server is configured to accept connections only using this key. It does not provide a pty or accept any input, so it is not possible to execute commands on the agent even if your private key is compromised. ## FAQ / Troubleshooting diff --git a/records.go b/records.go index 80a53a4..bdae715 100644 --- a/records.go +++ b/records.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "math" + "reflect" "time" "github.com/pocketbase/dbx" @@ -12,7 +13,7 @@ import ( "github.com/pocketbase/pocketbase/tools/types" ) -func createLongerRecords(shorterRecord *models.Record) { +func createLongerRecords(collectionName string, shorterRecord *models.Record) { shorterRecordType := shorterRecord.Get("type").(string) systemId := shorterRecord.Get("system").(string) // fmt.Println("create longer records", "recordType", shorterRecordType, "systemId", systemId) @@ -41,7 +42,7 @@ func createLongerRecords(shorterRecord *models.Record) { longerRecordPeriod := time.Now().UTC().Add(timeAgo + 10*time.Second).Format("2006-01-02 15:04:05") // check creation time of last 10m record lastLongerRecord, err := app.Dao().FindFirstRecordByFilter( - "system_stats", + collectionName, "type = {:type} && system = {:system} && created > {:created}", dbx.Params{"type": longerRecordType, "system": systemId, "created": longerRecordPeriod}, ) @@ -53,7 +54,7 @@ func createLongerRecords(shorterRecord *models.Record) { // get shorter records from the past x minutes // shorterRecordPeriod := time.Now().UTC().Add(timeAgo + time.Second).Format("2006-01-02 15:04:05") allShorterRecords, err := app.Dao().FindRecordsByFilter( - "system_stats", + collectionName, "type = {:type} && system = {:system} && created > {:created}", "-created", -1, @@ -67,7 +68,7 @@ func createLongerRecords(shorterRecord *models.Record) { } // average the shorter records and create 10m record averagedStats := averageSystemStats(allShorterRecords) - collection, _ := app.Dao().FindCollectionByNameOrId("system_stats") + collection, _ := app.Dao().FindCollectionByNameOrId(collectionName) tenMinRecord := models.NewRecord(collection) tenMinRecord.Set("system", systemId) tenMinRecord.Set("stats", averagedStats) @@ -78,65 +79,66 @@ func createLongerRecords(shorterRecord *models.Record) { } -// func averageSystemStats(records []*models.Record) SystemStats { -// numStats := len(records) -// firstStats := records[0].Get("stats").(SystemStats) -// sum := reflect.New(reflect.TypeOf(firstStats)).Elem() - -// for _, record := range records { -// stats := record.Get("stats").(SystemStats) -// statValue := reflect.ValueOf(stats) -// for i := 0; i < statValue.NumField(); i++ { -// field := sum.Field(i) -// field.SetFloat(field.Float() + statValue.Field(i).Float()) -// } -// } - -// average := reflect.New(reflect.TypeOf(firstStats)).Elem() -// for i := 0; i < sum.NumField(); i++ { -// average.Field(i).SetFloat(twoDecimals(sum.Field(i).Float() / float64(numStats))) -// } - -// return average.Interface().(SystemStats) -// } - +// calculate the average of a list of SystemStats using reflection func averageSystemStats(records []*models.Record) SystemStats { - var sum SystemStats count := float64(len(records)) + sum := reflect.New(reflect.TypeOf(SystemStats{})).Elem() for _, record := range records { var stats SystemStats json.Unmarshal([]byte(record.Get("stats").(types.JsonRaw)), &stats) - sum.Cpu += stats.Cpu - sum.Mem += stats.Mem - sum.MemUsed += stats.MemUsed - sum.MemPct += stats.MemPct - sum.MemBuffCache += stats.MemBuffCache - sum.Disk += stats.Disk - sum.DiskUsed += stats.DiskUsed - sum.DiskPct += stats.DiskPct - sum.DiskRead += stats.DiskRead - sum.DiskWrite += stats.DiskWrite - sum.NetworkSent += stats.NetworkSent - sum.NetworkRecv += stats.NetworkRecv + statValue := reflect.ValueOf(stats) + for i := 0; i < statValue.NumField(); i++ { + field := sum.Field(i) + field.SetFloat(field.Float() + statValue.Field(i).Float()) + } } - return SystemStats{ - Cpu: twoDecimals(sum.Cpu / count), - Mem: twoDecimals(sum.Mem / count), - MemUsed: twoDecimals(sum.MemUsed / count), - MemPct: twoDecimals(sum.MemPct / count), - MemBuffCache: twoDecimals(sum.MemBuffCache / count), - Disk: twoDecimals(sum.Disk / count), - DiskUsed: twoDecimals(sum.DiskUsed / count), - DiskPct: twoDecimals(sum.DiskPct / count), - DiskRead: twoDecimals(sum.DiskRead / count), - DiskWrite: twoDecimals(sum.DiskWrite / count), - NetworkSent: twoDecimals(sum.NetworkSent / count), - NetworkRecv: twoDecimals(sum.NetworkRecv / count), + average := reflect.New(reflect.TypeOf(SystemStats{})).Elem() + for i := 0; i < sum.NumField(); i++ { + average.Field(i).SetFloat(twoDecimals(sum.Field(i).Float() / count)) } + + return average.Interface().(SystemStats) } +// func averageSystemStats(records []*models.Record) SystemStats { +// var sum SystemStats +// count := float64(len(records)) + +// for _, record := range records { +// var stats SystemStats +// json.Unmarshal([]byte(record.Get("stats").(types.JsonRaw)), &stats) +// sum.Cpu += stats.Cpu +// sum.Mem += stats.Mem +// sum.MemUsed += stats.MemUsed +// sum.MemPct += stats.MemPct +// sum.MemBuffCache += stats.MemBuffCache +// sum.Disk += stats.Disk +// sum.DiskUsed += stats.DiskUsed +// sum.DiskPct += stats.DiskPct +// sum.DiskRead += stats.DiskRead +// sum.DiskWrite += stats.DiskWrite +// sum.NetworkSent += stats.NetworkSent +// sum.NetworkRecv += stats.NetworkRecv +// } + +// return SystemStats{ +// Cpu: twoDecimals(sum.Cpu / count), +// Mem: twoDecimals(sum.Mem / count), +// MemUsed: twoDecimals(sum.MemUsed / count), +// MemPct: twoDecimals(sum.MemPct / count), +// MemBuffCache: twoDecimals(sum.MemBuffCache / count), +// Disk: twoDecimals(sum.Disk / count), +// DiskUsed: twoDecimals(sum.DiskUsed / count), +// DiskPct: twoDecimals(sum.DiskPct / count), +// DiskRead: twoDecimals(sum.DiskRead / count), +// DiskWrite: twoDecimals(sum.DiskWrite / count), +// NetworkSent: twoDecimals(sum.NetworkSent / count), +// NetworkRecv: twoDecimals(sum.NetworkRecv / count), +// } +// } + /* Round float to two decimals */ func twoDecimals(value float64) float64 { return math.Round(value*100) / 100 @@ -148,7 +150,7 @@ func deleteOldRecords(collection string, recordType string, timeLimit time.Durat timeLimitStamp := time.Now().UTC().Add(timeLimit).Format("2006-01-02 15:04:05") records, _ := app.Dao().FindRecordsByExpr(collection, dbx.NewExp("type = {:type}", dbx.Params{"type": recordType}), - dbx.NewExp("created < {:created}", dbx.Params{"created": timeLimitStamp}), + dbx.NewExp("created > {:created}", dbx.Params{"created": timeLimitStamp}), ) for _, record := range records { if err := app.Dao().DeleteRecord(record); err != nil { diff --git a/site/embed.go b/site/embed.go index 41a0d58..fba9ace 100644 --- a/site/embed.go +++ b/site/embed.go @@ -1,17 +1,15 @@ +// Package site handles the Beszel frontend embedding. package site import ( "embed" - "io/fs" + + "github.com/labstack/echo/v5" ) //go:embed all:dist var assets embed.FS -func Assets() (fs.FS, error) { - return fs.Sub(assets, "dist") -} +var Dist = echo.MustSubFS(assets, "dist") -func Icons() (fs.FS, error) { - return fs.Sub(assets, "dist/icons") -} +var Icons = echo.MustSubFS(assets, "dist/icons") diff --git a/site/src/components/routes/server.tsx b/site/src/components/routes/server.tsx index 4555ff0..690ca70 100644 --- a/site/src/components/routes/server.tsx +++ b/site/src/components/routes/server.tsx @@ -73,12 +73,16 @@ export default function ServerDetail({ name }: { name: string }) { // get stats useEffect(() => { - if (!server.id) { + if (!server.id || !chartTime) { return } pb.collection('system_stats') .getFullList({ - filter: `system="${server.id}" && created > "${getPbTimestamp('1h')}"`, + filter: pb.filter('system={:id} && created > {:created} && type={:type}', { + id: server.id, + created: getPbTimestamp(chartTime), + type: chartTimeData[chartTime].type, + }), fields: 'created,stats', sort: 'created', }) @@ -86,7 +90,7 @@ export default function ServerDetail({ name }: { name: string }) { // console.log('sctats', records) setServerStats(records) }) - }, [server]) + }, [server, chartTime]) useEffect(() => { if (updatedSystem.id === server.id) { diff --git a/site/src/lib/utils.ts b/site/src/lib/utils.ts index 93e0882..4fc4d1a 100644 --- a/site/src/lib/utils.ts +++ b/site/src/lib/utils.ts @@ -123,26 +123,31 @@ export function getPbTimestamp(timeString: ChartTimes) { export const chartTimeData = { '1h': { + type: '1m', label: '1 hour', format: (timestamp: string) => hourWithMinutes(timestamp), getOffset: (endTime: Date) => timeHour.offset(endTime, -1), }, '12h': { + type: '10m', label: '12 hours', format: (timestamp: string) => hourWithMinutes(timestamp), getOffset: (endTime: Date) => timeHour.offset(endTime, -12), }, '24h': { + type: '20m', label: '24 hours', format: (timestamp: string) => hourWithMinutes(timestamp), getOffset: (endTime: Date) => timeHour.offset(endTime, -24), }, '1w': { + type: '120m', label: '1 week', format: (timestamp: string) => formatDay(timestamp), getOffset: (endTime: Date) => timeDay.offset(endTime, -7), }, '30d': { + type: '480m', label: '30 days', format: (timestamp: string) => formatDay(timestamp), getOffset: (endTime: Date) => timeDay.offset(endTime, -30),