diff --git a/README.md b/README.md index 15f16295..773cb1fe 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ These only apply to the development version of Sylve, the production version wil # Runtime Requirements -Sylve is designed to run on FreeBSD 14.3 or later, and it is recommended to use the latest version of FreeBSD for the best experience. +Sylve is designed to run on FreeBSD 15.0 or later, and it is recommended to use the latest version of FreeBSD for the best experience. ## Dependencies @@ -54,11 +54,11 @@ Running Sylve is pretty easy, but `sylve` depends on some packages that you can | -------------- | ------------ | -------- | -------- | ------------------------------------------------ | | smartmontools | 7.4_2 | No | No | Disk health monitoring | | tmux | 3.2 | No | No | Terminal multiplexer, used for the (web) console | -| libvirt | 11.1.0 | No | No | Virtualization API, used for Bhyve | -| bhyve-firmware | 1.0_2 | No | No | Collection of Firmware for bhyve | -| samba419 | 4.19.9_9 | No | No | SMB file sharing service | -| jansson | 2.14.1 | No | No | JSON library for C | -| swtpm | 0.10.1 | No | No | TPM emulator for VMs | +| libvirt | 11.7.0 | No | Yes | Virtualization API, used for Bhyve | +| bhyve-firmware | 1.0_2 | No | Yes | Collection of Firmware for bhyve | +| samba4XX | 4.XX | No | Yes | SMB file sharing service | +| swtpm | 0.10.1 | No | Yes | TPM emulator for VMs | +| jansson | 2.14.1 | No | No | C library for JSON parsing | We also need to enable some services in order to run Sylve, you can drop these into `/etc/rc.conf` if you don't have it already: @@ -66,15 +66,12 @@ We also need to enable some services in order to run Sylve, you can drop these i sysrc ntpd_enable="YES" # Optional sysrc ntpd_sync_on_start="YES" # Optional sysrc zfs_enable="YES" -sysrc libvirtd_enable="YES" -sysrc dnsmasq_enable="YES" -sysrc rpcbind_enable="YES" -sysrc nfs_server_enable="YES" -sysrc mountd_enable="YES" -sysrc samba_server_enable="YES" +sysrc libvirtd_enable="YES" # Optional +sysrc dnsmasq_enable="YES" # Optional +sysrc samba_server_enable="YES" # Optional ``` -Enabling `rctl` is required. Do this by adding the following line to `/boot/loader.conf`: +Enabling `rctl` is required if you're using Jails with Sylve. Do this by adding the following line to `/boot/loader.conf`: ```sh kern.racct.enable=1 @@ -91,7 +88,7 @@ kern.racct.enable=1 Install required packages. ```sh -pkg install git node20 npm-node20 go tmux libvirt bhyve-firmware smartmontools tmux samba419 jansson swtpm +pkg install git node22 npm-node22 go tmux libvirt bhyve-firmware smartmontools tmux samba422 jansson swtpm ``` Clone the repo and build Sylve. @@ -110,12 +107,13 @@ cp -rf ../config.example.json config.json # Edit the config.json file to your li ./sylve ``` -> In order to download an ISO go to: -> Datacenter > Your Host > Utilities > Downloader > + NEW +# Notes -> [!IMPORTANT] -> Bhyve does not support boot orders, so you cannot add installation media after creating the VM. -> So make sure to add an installation media when creating a new VM. +1. Bhyve doesn't support bootorders yet + +Since Bhyve doesn't support bootorders yet, you'll need to configure the order using the UEFI boot menu. You can download + +2. ARM64 support is still pending for Libvirt so the support is not there yet, for everything else it should just work out of the box. # Contributing diff --git a/internal/db/db.go b/internal/db/db.go index 43692694..7ef767f0 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -114,8 +114,6 @@ func SetupDatabase(cfg *internal.SylveConfig, isTest bool) *gorm.DB { &clusterModels.Cluster{}, &clusterModels.ClusterNode{}, - &clusterModels.ClusterS3Config{}, - &clusterModels.ClusterDirectoryConfig{}, &clusterModels.ClusterOption{}, &clusterModels.ClusterNote{}, ) diff --git a/internal/db/models/cluster/raft.go b/internal/db/models/cluster/raft.go index e0810b0f..5ab11a5a 100644 --- a/internal/db/models/cluster/raft.go +++ b/internal/db/models/cluster/raft.go @@ -75,10 +75,8 @@ func (f *FSMDispatcher) Apply(l *raft.Log) any { // ClusterSnapshot represents the state that will be snapshotted/restored type ClusterSnapshot struct { - Notes []ClusterNote `json:"notes"` - Options []ClusterOption `json:"options"` - S3Configs []ClusterS3Config `json:"s3Configs"` - DirectoryConfigs []ClusterDirectoryConfig `json:"directoryConfigs"` + Notes []ClusterNote `json:"notes"` + Options []ClusterOption `json:"options"` // We can add more tables here as needed } @@ -92,12 +90,6 @@ func (f *FSMDispatcher) Snapshot() (raft.FSMSnapshot, error) { if err := f.DB.Find(&snap.Options).Error; err != nil { return nil, err } - if err := f.DB.Find(&snap.S3Configs).Error; err != nil { - return nil, err - } - if err := f.DB.Find(&snap.DirectoryConfigs).Error; err != nil { - return nil, err - } return &snap, nil } @@ -118,8 +110,6 @@ func (f *FSMDispatcher) Restore(rc io.ReadCloser) error { sets := []restoreSet{ {"cluster_notes", snap.Notes, 500}, {"cluster_options", snap.Options, 100}, - {"cluster_s3_configs", snap.S3Configs, 100}, - {"cluster_directory_configs", snap.DirectoryConfigs, 100}, // We can add more tables here as needed } @@ -183,76 +173,6 @@ func RegisterDefaultHandlers(fsm *FSMDispatcher) { } }) - fsm.Register("s3Configs", func(db *gorm.DB, action string, raw json.RawMessage) error { - var s3Config ClusterS3Config - switch action { - case "create": - if err := json.Unmarshal(raw, &s3Config); err != nil { - return err - } - return upsertS3Cfg(db, &s3Config) - case "update": - if err := json.Unmarshal(raw, &s3Config); err != nil { - return err - } - return db.Model(&ClusterS3Config{}). - Where("id = ?", s3Config.ID). - Updates(s3Config).Error - case "delete": - var payload struct{ ID int } - if err := json.Unmarshal(raw, &payload); err != nil { - return err - } - return db.Delete(&ClusterS3Config{}, payload.ID).Error - case "bulk_delete": - var payload struct{ IDs []int } - if err := json.Unmarshal(raw, &payload); err != nil { - return err - } - if len(payload.IDs) > 0 { - return db.Delete(&ClusterS3Config{}, payload.IDs).Error - } - return nil - default: - return nil - } - }) - - fsm.Register("directoryConfigs", func(db *gorm.DB, action string, raw json.RawMessage) error { - var dirConfig ClusterDirectoryConfig - switch action { - case "create": - if err := json.Unmarshal(raw, &dirConfig); err != nil { - return err - } - return upsertDirCfg(db, &dirConfig) - case "update": - if err := json.Unmarshal(raw, &dirConfig); err != nil { - return err - } - return db.Model(&ClusterDirectoryConfig{}). - Where("id = ?", dirConfig.ID). - Updates(dirConfig).Error - case "delete": - var payload struct{ ID int } - if err := json.Unmarshal(raw, &payload); err != nil { - return err - } - return db.Delete(&ClusterDirectoryConfig{}, payload.ID).Error - case "bulk_delete": - var payload struct{ IDs []int } - if err := json.Unmarshal(raw, &payload); err != nil { - return err - } - if len(payload.IDs) > 0 { - return db.Delete(&ClusterDirectoryConfig{}, payload.IDs).Error - } - return nil - default: - return nil - } - }) - fsm.Register("options", func(db *gorm.DB, action string, raw json.RawMessage) error { var opt ClusterOption if err := json.Unmarshal(raw, &opt); err != nil { diff --git a/internal/db/models/cluster/storage.go b/internal/db/models/cluster/storage.go index a058e8bd..0d96f0b1 100644 --- a/internal/db/models/cluster/storage.go +++ b/internal/db/models/cluster/storage.go @@ -7,64 +7,3 @@ // under sponsorship from the FreeBSD Foundation. package clusterModels - -import ( - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -type ClusterS3Config struct { - ID uint `gorm:"primaryKey" json:"id"` - Name string `gorm:"uniqueIndex" json:"name"` - Endpoint string `json:"endpoint"` - Region string `json:"region"` - Bucket string `json:"bucket"` - AccessKey string `json:"accessKey"` - SecretKey string `json:"secretKey"` -} - -type ClusterDirectoryConfig struct { - ID uint `gorm:"primaryKey" json:"id"` - Name string `gorm:"uniqueIndex" json:"name"` - Path string `json:"path"` -} - -func upsertS3Cfg(db *gorm.DB, n *ClusterS3Config) error { - return db.Transaction(func(tx *gorm.DB) error { - if n.ID == 0 { - var next uint - if err := tx. - Table("cluster_s3_configs"). - Select("COALESCE(MAX(id), 0) + 1"). - Scan(&next).Error; err != nil { - return err - } - n.ID = next - } - - return tx.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "id"}}, - DoUpdates: clause.AssignmentColumns([]string{"name", "endpoint", "region", "bucket", "access_key", "secret_key"}), - }).Create(n).Error - }) -} - -func upsertDirCfg(db *gorm.DB, n *ClusterDirectoryConfig) error { - return db.Transaction(func(tx *gorm.DB) error { - if n.ID == 0 { - var next uint - if err := tx. - Table("cluster_directory_configs"). - Select("COALESCE(MAX(id), 0) + 1"). - Scan(&next).Error; err != nil { - return err - } - n.ID = next - } - - return tx.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "id"}}, - DoUpdates: clause.AssignmentColumns([]string{"name", "path"}), - }).Create(n).Error - }) -} diff --git a/internal/db/models/jail/jail.go b/internal/db/models/jail/jail.go index 34296a5d..9b19f1b9 100644 --- a/internal/db/models/jail/jail.go +++ b/internal/db/models/jail/jail.go @@ -80,6 +80,14 @@ type JailStats struct { CreatedAt time.Time `json:"createdAt" gorm:"autoCreateTime"` } +func (j JailStats) GetID() uint { + return j.ID +} + +func (j JailStats) GetCreatedAt() time.Time { + return j.CreatedAt +} + type Storage struct { ID uint `gorm:"primaryKey" json:"id"` JailID uint `json:"jid" gorm:"column:jid;index"` diff --git a/internal/handlers/cluster/storage.go b/internal/handlers/cluster/storage.go index bc69f937..f6b6a30a 100644 --- a/internal/handlers/cluster/storage.go +++ b/internal/handlers/cluster/storage.go @@ -7,41 +7,3 @@ // under sponsorship from the FreeBSD Foundation. package clusterHandlers - -import ( - "github.com/alchemillahq/sylve/internal" - clusterServiceInterfaces "github.com/alchemillahq/sylve/internal/interfaces/services/cluster" - "github.com/alchemillahq/sylve/internal/services/cluster" - "github.com/gin-gonic/gin" -) - -// @Summary Get Cluster Storages -// @Description Get all storage backends configured in the cluster (S3, etc.) -// @Tags Cluster -// @Accept json -// @Produce json -// @Security BearerAuth -// @Success 200 {object} internal.APIResponse[clusterServiceInterfaces.Storages] "Success" -// @Failure 500 {object} internal.APIResponse[any] "Internal Server Error" -// @Router /cluster/storage [get] -func Storages(cS *cluster.Service) gin.HandlerFunc { - return func(c *gin.Context) { - storages, err := cS.ListStorages() - if err != nil { - c.JSON(500, internal.APIResponse[any]{ - Status: "error", - Message: "list_storages_failed", - Error: err.Error(), - Data: nil, - }) - return - } - - c.JSON(200, internal.APIResponse[clusterServiceInterfaces.Storages]{ - Status: "success", - Message: "storages_listed", - Error: "", - Data: storages, - }) - } -} diff --git a/internal/handlers/cluster/storage_dir.go b/internal/handlers/cluster/storage_dir.go index 2280c64b..f6b6a30a 100644 --- a/internal/handlers/cluster/storage_dir.go +++ b/internal/handlers/cluster/storage_dir.go @@ -7,170 +7,3 @@ // under sponsorship from the FreeBSD Foundation. package clusterHandlers - -import ( - "strconv" - - "github.com/alchemillahq/sylve/internal" - "github.com/alchemillahq/sylve/internal/services/cluster" - "github.com/gin-gonic/gin" - "github.com/hashicorp/raft" -) - -type CreateDirStorageRequest struct { - Name string `json:"name" binding:"required,min=3"` - Path string `json:"path" binding:"required"` -} - -// @Summary Create a Directory Storage -// @Description Create a new Directory storage configuration in the cluster -// @Tags Cluster -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body CreateDirStorageRequest true "Create Directory Storage Request" -// @Success 200 {object} internal.APIResponse[any] "Success" -// @Failure 400 {object} internal.APIResponse[any] "Bad Request" -// @Failure 409 {object} internal.APIResponse[any] "Conflict" -// @Failure 500 {object} internal.APIResponse[any] "Internal Server Error" -// @Router /cluster/storage/directory [post] -func CreateDirStorage(cS *cluster.Service) gin.HandlerFunc { - return func(c *gin.Context) { - if cS.Raft == nil { - var req CreateDirStorageRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(400, internal.APIResponse[any]{ - Status: "error", - Message: "invalid_request", - Error: err.Error(), - Data: nil, - }) - return - } - - if err := cS.ProposeDirectoryConfig( - req.Name, req.Path, true, - ); err != nil { - c.JSON(500, internal.APIResponse[any]{ - Status: "error", - Message: "storage_create_failed", - Error: err.Error(), - Data: nil, - }) - return - } - - c.JSON(200, internal.APIResponse[any]{ - Status: "success", - Message: "storage_created", - Error: "", - Data: nil, - }) - return - } - - if cS.Raft.State() != raft.Leader { - forwardToLeader(c, cS) - return - } - - var req CreateDirStorageRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(400, internal.APIResponse[any]{ - Status: "error", - Message: "invalid_request", - Error: err.Error(), - Data: nil, - }) - return - } - - if err := cS.ProposeDirectoryConfig( - req.Name, req.Path, false, - ); err != nil { - c.JSON(500, internal.APIResponse[any]{ - Status: "error", - Message: "storage_create_failed", - Error: err.Error(), - Data: nil, - }) - return - } - - c.JSON(200, internal.APIResponse[any]{ - Status: "success", - Message: "storage_created", - Error: "", - Data: nil, - }) - } -} - -// @Summary Delete a Directory Storage -// @Description Delete a Directory storage configuration from the cluster by ID -// @Tags Cluster -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param id path int true "Storage ID" -// @Success 200 {object} internal.APIResponse[any] "Success" -// @Failure 400 {object} internal.APIResponse[any] "Bad Request" -// @Failure 500 {object} internal.APIResponse[any] "Internal Server Error" -// @Router /cluster/storage/directory/{id} [delete] -func DeleteDirStorage(cS *cluster.Service) gin.HandlerFunc { - return func(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.Atoi(idStr) - if err != nil || id <= 0 { - c.JSON(400, internal.APIResponse[any]{ - Status: "error", - Message: "invalid_id", - Error: "id must be a positive integer", - Data: nil, - }) - return - } - - if cS.Raft == nil { - if err := cS.ProposeDirectoryConfigDelete(uint(id), true); err != nil { - c.JSON(500, internal.APIResponse[any]{ - Status: "error", - Message: "storage_delete_failed", - Error: err.Error(), - Data: nil, - }) - return - } - - c.JSON(200, internal.APIResponse[any]{ - Status: "success", - Message: "storage_deleted", - Error: "", - Data: nil, - }) - return - } - - if cS.Raft.State() != raft.Leader { - forwardToLeader(c, cS) - return - } - - if err := cS.ProposeDirectoryConfigDelete(uint(id), false); err != nil { - c.JSON(500, internal.APIResponse[any]{ - Status: "error", - Message: "storage_delete_failed", - Error: err.Error(), - Data: nil, - }) - return - } - - c.JSON(200, internal.APIResponse[any]{ - Status: "success", - Message: "storage_deleted", - Error: "", - Data: nil, - }) - } -} diff --git a/internal/handlers/cluster/storage_s3.go b/internal/handlers/cluster/storage_s3.go index 9234aed9..f6b6a30a 100644 --- a/internal/handlers/cluster/storage_s3.go +++ b/internal/handlers/cluster/storage_s3.go @@ -7,175 +7,3 @@ // under sponsorship from the FreeBSD Foundation. package clusterHandlers - -import ( - "strconv" - - "github.com/alchemillahq/sylve/internal" - "github.com/alchemillahq/sylve/internal/services/cluster" - "github.com/gin-gonic/gin" - "github.com/hashicorp/raft" -) - -type CreateS3StorageRequest struct { - Name string `json:"name" binding:"required,min=3"` - Endpoint string `json:"endpoint" binding:"required"` - Region string `json:"region" binding:"required"` - Bucket string `json:"bucket" binding:"required"` - AccessKey string `json:"accessKey" binding:"required"` - SecretKey string `json:"secretKey" binding:"required"` -} - -// @Summary Create an S3 Storage -// @Description Create a new S3 storage configuration in the cluster -// @Tags Cluster -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body CreateS3StorageRequest true "Create S3 Storage Request" -// @Success 200 {object} internal.APIResponse[any] "Success" -// @Failure 400 {object} internal.APIResponse[any] "Bad Request" -// @Failure 409 {object} internal.APIResponse[any] "Conflict" -// @Failure 500 {object} internal.APIResponse[any] "Internal Server Error" -// @Router /cluster/storage/s3 [post] -func CreateS3Storage(cS *cluster.Service) gin.HandlerFunc { - return func(c *gin.Context) { - if cS.Raft == nil { - var req CreateS3StorageRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(400, internal.APIResponse[any]{ - Status: "error", - Message: "invalid_request", - Error: err.Error(), - Data: nil, - }) - return - } - - if err := cS.ProposeS3Config( - req.Name, req.Endpoint, req.Region, req.Bucket, req.AccessKey, req.SecretKey, true, - ); err != nil { - c.JSON(500, internal.APIResponse[any]{ - Status: "error", - Message: "storage_create_failed", - Error: err.Error(), - Data: nil, - }) - return - } - - c.JSON(200, internal.APIResponse[any]{ - Status: "success", - Message: "storage_created", - Error: "", - Data: nil, - }) - return - } - - if cS.Raft.State() != raft.Leader { - forwardToLeader(c, cS) - return - } - - var req CreateS3StorageRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(400, internal.APIResponse[any]{ - Status: "error", - Message: "invalid_request", - Error: err.Error(), - Data: nil, - }) - return - } - - if err := cS.ProposeS3Config( - req.Name, req.Endpoint, req.Region, req.Bucket, req.AccessKey, req.SecretKey, - false, - ); err != nil { - c.JSON(500, internal.APIResponse[any]{ - Status: "error", - Message: "storage_create_failed", - Error: err.Error(), - Data: nil, - }) - return - } - - c.JSON(200, internal.APIResponse[any]{ - Status: "success", - Message: "storage_created", - Error: "", - Data: nil, - }) - } -} - -// @Summary Delete an S3 Storage -// @Description Delete an S3 storage configuration from the cluster by ID -// @Tags Cluster -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param id path int true "Storage ID" -// @Success 200 {object} internal.APIResponse[any] "Success" -// @Failure 400 {object} internal.APIResponse[any] "Bad Request" -// @Failure 500 {object} internal.APIResponse[any] "Internal Server Error" -// @Router /cluster/storage/s3/{id} [delete] -func DeleteS3Storage(cS *cluster.Service) gin.HandlerFunc { - return func(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.Atoi(idStr) - if err != nil || id <= 0 { - c.JSON(400, internal.APIResponse[any]{ - Status: "error", - Message: "invalid_id", - Error: "id must be a positive integer", - Data: nil, - }) - return - } - - if cS.Raft == nil { - if err := cS.ProposeS3ConfigDelete(uint(id), true); err != nil { - c.JSON(500, internal.APIResponse[any]{ - Status: "error", - Message: "storage_delete_failed", - Error: err.Error(), - Data: nil, - }) - return - } - - c.JSON(200, internal.APIResponse[any]{ - Status: "success", - Message: "storage_deleted", - Error: "", - Data: nil, - }) - return - } - - if cS.Raft.State() != raft.Leader { - forwardToLeader(c, cS) - return - } - - if err := cS.ProposeS3ConfigDelete(uint(id), false); err != nil { - c.JSON(500, internal.APIResponse[any]{ - Status: "error", - Message: "storage_delete_failed", - Error: err.Error(), - Data: nil, - }) - return - } - - c.JSON(200, internal.APIResponse[any]{ - Status: "success", - Message: "storage_deleted", - Error: "", - Data: nil, - }) - } -} diff --git a/internal/handlers/jail/stats.go b/internal/handlers/jail/stats.go index 02fda907..d468f3c2 100644 --- a/internal/handlers/jail/stats.go +++ b/internal/handlers/jail/stats.go @@ -12,6 +12,7 @@ import ( "strconv" "github.com/alchemillahq/sylve/internal" + "github.com/alchemillahq/sylve/internal/db" jailModels "github.com/alchemillahq/sylve/internal/db/models/jail" jailServiceInterfaces "github.com/alchemillahq/sylve/internal/interfaces/services/jail" "github.com/alchemillahq/sylve/internal/services/jail" @@ -165,7 +166,7 @@ func GetJailLogs(jailService *jail.Service) gin.HandlerFunc { } // @Summary Get Jail Statistics -// @Description Retrieve statistics for a specific jail +// @Description Retrieve statistics for a jail by CTID and GFS step // @Tags Jail // @Accept json // @Produce json @@ -173,26 +174,23 @@ func GetJailLogs(jailService *jail.Service) gin.HandlerFunc { // @Success 200 {object} internal.APIResponse[[]jailModels.JailStats] "Success" // @Failure 400 {object} internal.APIResponse[any] "Bad Request" // @Failure 500 {object} internal.APIResponse[any] "Internal Server Error" -// @Router /jail/stats/:ctId/:limit [get] +// @Router /jail/stats/:ctid/:step [get] func GetJailStats(jailService *jail.Service) gin.HandlerFunc { return func(c *gin.Context) { - ctId := c.Param("ctId") - limit := c.Param("limit") - if ctId == "" || limit == "" { + ctId, _ := utils.ParamUint(c, "ctId") + step := c.Param("step") + + if ctId == 0 || step == "" { c.JSON(400, internal.APIResponse[any]{ Status: "error", Message: "invalid_request", Data: nil, - Error: "ctId and limit are required", + Error: "ctid and step are required", }) return } - stats, err := jailService.GetJailUsage( - uint(utils.StringToUint64(ctId)), - int(utils.StringToUint64(limit)), - ) - + stats, err := jailService.GetJailUsage(ctId, db.GFSStep(step)) if err != nil { c.JSON(500, internal.APIResponse[any]{ Status: "error", diff --git a/internal/handlers/routes.go b/internal/handlers/routes.go index cbb627ab..af15a51f 100644 --- a/internal/handlers/routes.go +++ b/internal/handlers/routes.go @@ -327,7 +327,7 @@ func RegisterRoutes(r *gin.Engine, jail.GET("/:id/logs", jailHandlers.GetJailLogs(jailService)) jail.PUT("/memory", jailHandlers.UpdateJailMemory(jailService)) jail.PUT("/cpu", jailHandlers.UpdateJailCPU(jailService)) - jail.GET("/stats/:ctId/:limit", jailHandlers.GetJailStats(jailService)) + jail.GET("/stats/:ctId/:step", jailHandlers.GetJailStats(jailService)) jail.PUT("/resource-limits/:ctId", jailHandlers.UpdateResourceLimits(jailService)) jail.POST("", jailHandlers.CreateJail(jailService)) @@ -408,17 +408,6 @@ func RegisterRoutes(r *gin.Engine, clusterNotes.DELETE("/:id", clusterHandlers.DeleteNote(clusterService)) } - clusterStorages := cluster.Group("/storage") - { - clusterStorages.GET("", clusterHandlers.Storages(clusterService)) - - clusterStorages.POST("/s3", clusterHandlers.CreateS3Storage(clusterService)) - clusterStorages.DELETE("/s3/:id", clusterHandlers.DeleteS3Storage(clusterService)) - - clusterStorages.POST("/directory", clusterHandlers.CreateDirStorage(clusterService)) - clusterStorages.DELETE("/directory/:id", clusterHandlers.DeleteDirStorage(clusterService)) - } - vnc := api.Group("/vnc") vnc.Use(EnsureCorrectHost(db)) vnc.Use(middleware.EnsureAuthenticated(authService)) diff --git a/internal/interfaces/services/cluster/storage.go b/internal/interfaces/services/cluster/storage.go index 485ecd1d..a951b16e 100644 --- a/internal/interfaces/services/cluster/storage.go +++ b/internal/interfaces/services/cluster/storage.go @@ -8,9 +8,4 @@ package clusterServiceInterfaces -import clusterModels "github.com/alchemillahq/sylve/internal/db/models/cluster" - -type Storages struct { - S3 []clusterModels.ClusterS3Config `json:"s3"` - Directories []clusterModels.ClusterDirectoryConfig `json:"directories"` -} +type Storages struct{} diff --git a/internal/interfaces/services/jail/jail.go b/internal/interfaces/services/jail/jail.go index 2fca3058..84cea032 100644 --- a/internal/interfaces/services/jail/jail.go +++ b/internal/interfaces/services/jail/jail.go @@ -117,6 +117,6 @@ type EditJailNetworkRequest struct { type JailServiceInterface interface { StoreJailUsage() error - PruneOrphanedJailStats([]uint) error + PruneOrphanedJailStats() error WatchNetworkObjectChanges() error } diff --git a/internal/services/cluster/cluster.go b/internal/services/cluster/cluster.go index 114044be..ac80043b 100644 --- a/internal/services/cluster/cluster.go +++ b/internal/services/cluster/cluster.go @@ -172,62 +172,6 @@ func (s *Service) backfillPreClusterState() error { } } - { - var s3cfgs []clusterModels.ClusterS3Config - if err := s.DB.Order("id ASC").Find(&s3cfgs).Error; err != nil { - return fmt.Errorf("scan_existing_s3cfgs: %w", err) - } - - for _, c := range s3cfgs { - payloadStruct := struct { - ID uint `json:"id"` - Name string `json:"name"` - Endpoint string `json:"endpoint"` - Region string `json:"region"` - Bucket string `json:"bucket"` - AccessKey string `json:"accessKey"` - SecretKey string `json:"secretKey"` - }{ - ID: c.ID, - Name: c.Name, - Endpoint: c.Endpoint, - Region: c.Region, - Bucket: c.Bucket, - AccessKey: c.AccessKey, - SecretKey: c.SecretKey, - } - - data, _ := json.Marshal(payloadStruct) - cmd := clusterModels.Command{Type: "s3Configs", Action: "create", Data: data} - if err := s.Raft.Apply(utils.MustJSON(cmd), 5*time.Second).Error(); err != nil { - return fmt.Errorf("apply_synth_create_s3cfg id=%d: %w", c.ID, err) - } - } - } - - { - var dircfgs []clusterModels.ClusterDirectoryConfig - if err := s.DB.Order("id ASC").Find(&dircfgs).Error; err != nil { - return fmt.Errorf("scan_existing_dircfgs: %w", err) - } - - for _, c := range dircfgs { - payloadStruct := struct { - ID uint `json:"id"` - Path string `json:"path"` - }{ - ID: c.ID, - Path: c.Path, - } - - data, _ := json.Marshal(payloadStruct) - cmd := clusterModels.Command{Type: "directoryConfigs", Action: "create", Data: data} - if err := s.Raft.Apply(utils.MustJSON(cmd), 5*time.Second).Error(); err != nil { - return fmt.Errorf("apply_synth_create_dircfg id=%d: %w", c.ID, err) - } - } - } - if err := s.Raft.Barrier(10 * time.Second).Error(); err != nil { return fmt.Errorf("barrier_after_backfill: %w", err) } diff --git a/internal/services/cluster/storage.go b/internal/services/cluster/storage.go index b25b01f6..f8ce9b5d 100644 --- a/internal/services/cluster/storage.go +++ b/internal/services/cluster/storage.go @@ -7,150 +7,3 @@ // under sponsorship from the FreeBSD Foundation. package cluster - -import ( - "encoding/json" - "fmt" - "time" - - clusterModels "github.com/alchemillahq/sylve/internal/db/models/cluster" - clusterServiceInterfaces "github.com/alchemillahq/sylve/internal/interfaces/services/cluster" - "github.com/alchemillahq/sylve/pkg/s3" -) - -func (s *Service) ListStorages() (clusterServiceInterfaces.Storages, error) { - var s3 []clusterModels.ClusterS3Config - var directories []clusterModels.ClusterDirectoryConfig - - err := s.DB.Order("id ASC").Find(&s3).Error - - if err != nil { - return clusterServiceInterfaces.Storages{}, err - } - - err = s.DB.Order("id ASC").Find(&directories).Error - if err != nil { - return clusterServiceInterfaces.Storages{}, err - } - - return clusterServiceInterfaces.Storages{S3: s3, Directories: directories}, nil -} - -func (s *Service) ProposeS3Config(name, - endpoint, - region, - bucket, - accessKey, - secretKey string, - bypassRaft bool) error { - err := s3.ValidateConfig(endpoint, region, bucket, accessKey, secretKey) - if err != nil { - return fmt.Errorf("s3_config_invalid: %w", err) - } - - if bypassRaft { - s3 := clusterModels.ClusterS3Config{ - Name: name, - Endpoint: endpoint, - Region: region, - Bucket: bucket, - AccessKey: accessKey, - SecretKey: secretKey, - } - - err := s.DB.Create(&s3).Error - if err != nil { - return err - } - - return nil - } - - if s.Raft == nil { - return fmt.Errorf("raft_not_initialized") - } - - payloadStruct := struct { - Name string `json:"name"` - Endpoint string `json:"endpoint"` - Region string `json:"region"` - Bucket string `json:"bucket"` - AccessKey string `json:"accessKey"` - SecretKey string `json:"secretKey"` - }{ - Name: name, - Endpoint: endpoint, - Region: region, - Bucket: bucket, - AccessKey: accessKey, - SecretKey: secretKey, - } - - data, err := json.Marshal(payloadStruct) - if err != nil { - return fmt.Errorf("failed_to_marshal_note_payload: %w", err) - } - - cmd := clusterModels.Command{ - Type: "s3Configs", - Action: "create", - Data: data, - } - - payload, err := json.Marshal(cmd) - if err != nil { - return fmt.Errorf("failed_to_marshal_command: %w", err) - } - - applyFuture := s.Raft.Apply(payload, 5*time.Second) - if err := applyFuture.Error(); err != nil { - return fmt.Errorf("raft_apply_failed: %w", err) - } - - if resp, ok := applyFuture.Response().(error); ok && resp != nil { - return fmt.Errorf("fsm_apply_failed: %w", resp) - } - - return nil -} - -func (s *Service) ProposeS3ConfigDelete(id uint, bypassRaft bool) error { - if bypassRaft { - return s.DB.Delete(&clusterModels.ClusterS3Config{}, id).Error - } - - if s.Raft == nil { - return fmt.Errorf("raft_not_initialized") - } - - payloadStruct := struct { - ID uint `json:"id"` - }{ID: id} - - data, err := json.Marshal(payloadStruct) - if err != nil { - return fmt.Errorf("failed_to_marshal_delete_payload: %w", err) - } - - cmd := clusterModels.Command{ - Type: "s3Configs", - Action: "delete", - Data: data, - } - - payload, err := json.Marshal(cmd) - if err != nil { - return fmt.Errorf("failed_to_marshal_command: %w", err) - } - - applyFuture := s.Raft.Apply(payload, 5*time.Second) - if err := applyFuture.Error(); err != nil { - return fmt.Errorf("raft_apply_failed: %w", err) - } - - if resp, ok := applyFuture.Response().(error); ok && resp != nil { - return fmt.Errorf("fsm_apply_failed: %w", resp) - } - - return nil -} diff --git a/internal/services/cluster/storage_dir.go b/internal/services/cluster/storage_dir.go index 18c39961..f8ce9b5d 100644 --- a/internal/services/cluster/storage_dir.go +++ b/internal/services/cluster/storage_dir.go @@ -7,118 +7,3 @@ // under sponsorship from the FreeBSD Foundation. package cluster - -import ( - "encoding/json" - "fmt" - "time" - - clusterModels "github.com/alchemillahq/sylve/internal/db/models/cluster" - "github.com/alchemillahq/sylve/pkg/utils" -) - -func (s *Service) ProposeDirectoryConfig(name string, path string, bypassRaft bool) error { - exists, err := utils.IsDir(path) - - if err != nil { - return fmt.Errorf("failed to check if directory exists: %w", err) - } - - if !exists { - return fmt.Errorf("directory does not exist: %s", path) - } - - if bypassRaft { - dir := clusterModels.ClusterDirectoryConfig{ - Name: name, - Path: path, - } - - err := s.DB.Create(&dir).Error - if err != nil { - return err - } - - return nil - } - - if s.Raft == nil { - return fmt.Errorf("raft_not_initialized") - } - - payloadStruct := struct { - Name string `json:"name"` - Path string `json:"path"` - }{ - Name: name, - Path: path, - } - - data, err := json.Marshal(payloadStruct) - if err != nil { - return fmt.Errorf("failed_to_marshal_note_payload: %w", err) - } - - cmd := clusterModels.Command{ - Type: "directoryConfigs", - Action: "create", - Data: data, - } - - payload, err := json.Marshal(cmd) - if err != nil { - return fmt.Errorf("failed_to_marshal_command: %w", err) - } - - applyFuture := s.Raft.Apply(payload, 5*time.Second) - if err := applyFuture.Error(); err != nil { - return fmt.Errorf("raft_apply_failed: %w", err) - } - - if resp, ok := applyFuture.Response().(error); ok && resp != nil { - return fmt.Errorf("fsm_apply_failed: %w", resp) - } - - return nil -} - -func (s *Service) ProposeDirectoryConfigDelete(id uint, bypassRaft bool) error { - if bypassRaft { - return s.DB.Delete(&clusterModels.ClusterDirectoryConfig{}, id).Error - } - - if s.Raft == nil { - return fmt.Errorf("raft_not_initialized") - } - - payloadStruct := struct { - ID uint `json:"id"` - }{ID: id} - - data, err := json.Marshal(payloadStruct) - if err != nil { - return fmt.Errorf("failed_to_marshal_payload: %w", err) - } - - cmd := clusterModels.Command{ - Type: "directoryConfigs", - Action: "delete", - Data: data, - } - - payload, err := json.Marshal(cmd) - if err != nil { - return fmt.Errorf("failed_to_marshal_command: %w", err) - } - - applyFuture := s.Raft.Apply(payload, 5*time.Second) - if err := applyFuture.Error(); err != nil { - return fmt.Errorf("raft_apply_failed: %w", err) - } - - if resp, ok := applyFuture.Response().(error); ok && resp != nil { - return fmt.Errorf("fsm_apply_failed: %w", resp) - } - - return nil -} diff --git a/internal/services/jail/stats.go b/internal/services/jail/stats.go index 0065eb24..0eecef0c 100644 --- a/internal/services/jail/stats.go +++ b/internal/services/jail/stats.go @@ -15,9 +15,12 @@ import ( "os/exec" "strconv" "strings" + "time" + "github.com/alchemillahq/sylve/internal/db" jailModels "github.com/alchemillahq/sylve/internal/db/models/jail" jailServiceInterfaces "github.com/alchemillahq/sylve/internal/interfaces/services/jail" + "github.com/alchemillahq/sylve/internal/logger" "github.com/alchemillahq/sylve/pkg/utils" cpuid "github.com/klauspost/cpuid/v2" @@ -170,6 +173,79 @@ func (s *Service) IsJailActive(ctId uint) (bool, error) { return false, nil } +func (s *Service) PruneOrphanedJailStats() error { + if err := s.DB. + Where( + "jid NOT IN (?)", + s.DB. + Model(&jailModels.Jail{}). + Select("id"), + ). + Delete(&jailModels.JailStats{}). + Error; err != nil { + return fmt.Errorf("failed to prune orphaned JailStats: %w", err) + } + return nil +} + +func (s *Service) ApplyJailStatsRetention() error { + var jailIDs []uint + if err := s.DB. + Model(&jailModels.JailStats{}). + Select("DISTINCT jid"). + Pluck("jid", &jailIDs).Error; err != nil { + return fmt.Errorf("failed_to_get_jail_ids_for_retention: %w", err) + } + + now := time.Now() + + for _, jailID := range jailIDs { + var stats []jailModels.JailStats + if err := s.DB. + Where("jid = ?", jailID). + Order("created_at ASC"). + Find(&stats).Error; err != nil { + return fmt.Errorf("failed_to_get_jail_stats_for_retention: %w", err) + } + + if len(stats) == 0 { + continue + } + + // Check if jail is active + var jail jailModels.Jail + if err := s.DB.Where("id = ?", jailID).First(&jail).Error; err != nil { + logger.L.Error().Err(err).Uint("jail_id", jailID).Msg("failed_to_get_jail_for_retention") + continue + } + + isActive, err := s.IsJailActive(jail.CTID) + if err != nil { + logger.L.Error().Err(err).Uint("jail_id", jailID).Msg("failed_to_check_if_jail_is_active_for_retention") + continue + } + + if isActive { + _, deleteIDs := db.ApplyGFS(now, stats) + if len(deleteIDs) == 0 { + continue + } + + if err := s.DB. + Where("id IN ?", deleteIDs). + Delete(&jailModels.JailStats{}).Error; err != nil { + return fmt.Errorf("failed_to_delete_old_jail_stats: %w", err) + } + } + } + + if err := s.PruneOrphanedJailStats(); err != nil { + return err + } + + return nil +} + func (s *Service) StoreJailUsage() error { if !s.crudMutex.TryLock() { return nil @@ -182,10 +258,8 @@ func (s *Service) StoreJailUsage() error { return fmt.Errorf("failed_to_load_jails: %w", err) } - jDBIDs := make([]uint, 0, len(jails)) - if len(jails) == 0 { - return s.PruneOrphanedJailStats(jDBIDs) + return s.ApplyJailStatsRetention() } states, err := s.GetStates() @@ -233,8 +307,11 @@ func (s *Service) StoreJailUsage() error { memPct = math.Round((float64(live.MemBytesUsed)/float64(sysRAM))*10000.0) / 100.0 } + cpuPct = math.Max(0, math.Min(100, cpuPct)) + memPct = math.Max(0, math.Min(100, memPct)) + stat := &jailModels.JailStats{ - JailID: (j.ID), + JailID: j.ID, CPUUsage: cpuPct, MemoryUsage: memPct, } @@ -244,58 +321,10 @@ func (s *Service) StoreJailUsage() error { } } - for _, j := range jails { - jDBIDs = append(jDBIDs, j.ID) - } - - for _, dbID := range jDBIDs { - var stats []jailModels.JailStats - if err := s.DB. - Where("jid = ?", dbID). - Order("id DESC"). - Limit(256). - Find(&stats).Error; err != nil { - return fmt.Errorf("failed_to_get_jail_stats: %w", err) - } - - if len(stats) < 256 { - continue - } - - cutoff := stats[len(stats)-1].ID - if err := s.DB. - Where("jid = ? AND id < ?", dbID, cutoff). - Delete(&jailModels.JailStats{}).Error; err != nil { - return fmt.Errorf("failed_to_delete_old_jail_stats: %w", err) - } - } - - if err := s.PruneOrphanedJailStats(jDBIDs); err != nil { - return err - } - - return nil + return s.ApplyJailStatsRetention() } -func (s *Service) PruneOrphanedJailStats(validJailIds []uint) error { - if len(validJailIds) == 0 { - return s.DB.Where("1 = 1").Delete(&jailModels.JailStats{}).Error - } - - valid := make([]int, len(validJailIds)) - for i, id := range validJailIds { - valid[i] = int(id) - } - - if err := s.DB. - Where("jid NOT IN ?", valid). - Delete(&jailModels.JailStats{}).Error; err != nil { - return fmt.Errorf("failed_to_prune_orphaned_jail_stats: %w", err) - } - return nil -} - -func (s *Service) GetJailUsage(ctId uint, limit int) ([]jailModels.JailStats, error) { +func (s *Service) GetJailUsage(ctId uint, step db.GFSStep) ([]jailModels.JailStats, error) { var jailId uint if err := s.DB.Model(&jailModels.Jail{}). Where("ct_id = ?", ctId). @@ -308,15 +337,18 @@ func (s *Service) GetJailUsage(ctId uint, limit int) ([]jailModels.JailStats, er return nil, fmt.Errorf("jail_not_found") } - var jailStats []jailModels.JailStats - sub := s.DB. - Model(&jailModels.JailStats{}). - Where("jid = ?", jailId). - Order("id DESC"). - Limit(limit) + window, err := step.Window() + if err != nil { + return nil, err + } - if err := s.DB.Table("(?) as sub", sub). - Order("id ASC"). + now := time.Now() + from := now.Add(-window) + + var jailStats []jailModels.JailStats + if err := s.DB. + Where("jid = ? AND created_at >= ?", jailId, from). + Order("created_at ASC"). Find(&jailStats).Error; err != nil { return nil, fmt.Errorf("failed_to_get_jail_usage: %w", err) } diff --git a/internal/services/network/objects.go b/internal/services/network/objects.go index 88a284aa..93fa419e 100644 --- a/internal/services/network/objects.go +++ b/internal/services/network/objects.go @@ -118,7 +118,7 @@ func validateValues(oType string, values []string) error { } if !utils.IsValidPort(vInt) { - return fmt.Errorf("invalid port value: %s", value) + return fmt.Errorf("invalid port value: %d", vInt) } } @@ -172,11 +172,11 @@ func (s *Service) IsObjectUsed(id uint) (bool, error) { } if err := s.DB.Find(&jailNetworks).Error; err != nil { - return true, fmt.Errorf("failed to find jail networks: %w", id, err) + return true, fmt.Errorf("failed to find jail networks: %d %w", id, err) } if err := s.DB.Preload("IPObject.Entries").Find(&dhcpLeases).Error; err != nil { - return true, fmt.Errorf("failed to find DHCP leases: %w", id, err) + return true, fmt.Errorf("failed to find DHCP leases: %d %w", id, err) } for _, sw := range switches { @@ -252,7 +252,7 @@ func (s *Service) IsObjectUsed(id uint) (bool, error) { } if err := s.DB.Preload("MACObject.Entries").Find(&dhcpLeases).Error; err != nil { - return true, fmt.Errorf("failed to find DHCP leases: %w", id, err) + return true, fmt.Errorf("failed to find DHCP leases: %d %w", id, err) } for _, dl := range dhcpLeases { @@ -290,7 +290,7 @@ func (s *Service) IsObjectUsed(id uint) (bool, error) { if object.Type == "DUID" { var dhcpLeases []networkModels.DHCPStaticLease if err := s.DB.Preload("DUIDObject.Entries").Find(&dhcpLeases).Error; err != nil { - return true, fmt.Errorf("failed to find DHCP leases: %w", id, err) + return true, fmt.Errorf("failed to find DHCP leases: %d %w", id, err) } for _, dl := range dhcpLeases { @@ -465,7 +465,7 @@ func (s *Service) EditObject(id uint, name string, oType string, values []string } if err := s.DB.Preload("MACObject.Entries").Where("mac_object_id = ?", id).Find(&dhcpLeases).Error; err != nil { - return fmt.Errorf("failed to find DHCP leases: %w", id, err) + return fmt.Errorf("failed to find DHCP leases: %d %w", id, err) } var vm vmModels.VM @@ -691,7 +691,7 @@ func (s *Service) EditObject(id uint, name string, oType string, values []string var dhcpLeases []networkModels.DHCPStaticLease if err := s.DB.Preload("IPObject.Entries").Where("ip_object_id = ?", id).Find(&dhcpLeases).Error; err != nil { - return fmt.Errorf("failed to find DHCP leases: %w", id, err) + return fmt.Errorf("failed to find DHCP leases: %d %w", id, err) } /* Object was used in DHCP leases, but now we're changing it to something else, we can't do that */ diff --git a/pkg/utils/sysctl/sysctl_test.go b/pkg/utils/sysctl/sysctl_test.go index 18e46db4..cd0f1abd 100644 --- a/pkg/utils/sysctl/sysctl_test.go +++ b/pkg/utils/sysctl/sysctl_test.go @@ -39,7 +39,7 @@ func TestGetInt64(t *testing.T) { // vm.swap_idle_enabled does NOT exist on macOS switch runtime.GOOS { case "freebsd": - key = "vm.swap_idle_enabled" + key = "vm.kmem_zmax" case "darwin": key = "kern.maxfiles" } diff --git a/web/package-lock.json b/web/package-lock.json index 756d0900..6df89fa8 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,10 +12,6 @@ "@fontsource/noto-sans": "^5.2.7", "@layerstack/svelte-stores": "^1.0.2", "@svelte-put/shortcut": "^4.1.0", - "@tanstack/query-async-storage-persister": "^5.90.12", - "@tanstack/svelte-query": "^6.0.8", - "@tanstack/svelte-query-devtools": "^6.0.0", - "@tanstack/svelte-query-persist-client": "^6.0.10", "@wuchale/svelte": "^0.17.5", "@wuchale/vite-plugin": "^0.15.3", "adze": "^2.2.4", @@ -2027,104 +2023,6 @@ "vite": "^5.2.0 || ^6 || ^7" } }, - "node_modules/@tanstack/query-async-storage-persister": { - "version": "5.90.14", - "resolved": "https://registry.npmjs.org/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.90.14.tgz", - "integrity": "sha512-oU+u40luly1PlrOrezeVxdF7AoF6dYw7BaU16Eg0+xLBwCRbW4cutxtxWG306Pgsnks5jIRDI4DjAPZKFnPSXQ==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.90.12", - "@tanstack/query-persist-client-core": "5.91.11" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.90.12", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.12.tgz", - "integrity": "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/query-devtools": { - "version": "5.91.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.91.1.tgz", - "integrity": "sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/query-persist-client-core": { - "version": "5.91.11", - "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.91.11.tgz", - "integrity": "sha512-NNpRGxQY/nVOdzfs5QbevPjGsUVoEiFwqxxaopLyu6todwtDOCfIOfhXSmpMVXBiCxUn7kqaUB1iwaBKqoAVRQ==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.90.12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/svelte-query": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@tanstack/svelte-query/-/svelte-query-6.0.10.tgz", - "integrity": "sha512-J0kM3JNvRcRCM6cbHLeICs73aLp98N/nsihdVEtiNo3MEN4pAnO45qZ2yxX70MrEZ9vffXaCXMCChwgXs1lZ/Q==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.90.12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "svelte": "^5.25.0" - } - }, - "node_modules/@tanstack/svelte-query-devtools": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@tanstack/svelte-query-devtools/-/svelte-query-devtools-6.0.2.tgz", - "integrity": "sha512-UQdtAPsdJQ2HijMVy+W+/CHyrUZmCkubfAi/sqWsx+1fdY+g7ONa5xroBLiaebC+5jJxvk9kij61QMUHzzJgQA==", - "license": "MIT", - "dependencies": { - "@tanstack/query-devtools": "5.91.1", - "esm-env": "^1.2.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "@tanstack/svelte-query": "^6.0.8", - "svelte": "^5.25.0" - } - }, - "node_modules/@tanstack/svelte-query-persist-client": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@tanstack/svelte-query-persist-client/-/svelte-query-persist-client-6.0.12.tgz", - "integrity": "sha512-eBPByyOu0jkFa4GWe9vukf1tqPXbeQ78V6y/EHrYiGjfA2txnwwAf+anyOcyjMtlZFO2N2X9rEUOXelG/77Xjg==", - "license": "MIT", - "dependencies": { - "@tanstack/query-persist-client-core": "5.91.11" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "@tanstack/svelte-query": "^6.0.10", - "svelte": "^5.25.0" - } - }, "node_modules/@thaunknown/thirty-two": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@thaunknown/thirty-two/-/thirty-two-1.0.5.tgz", diff --git a/web/package.json b/web/package.json index c37fd4e0..2eaebce1 100644 --- a/web/package.json +++ b/web/package.json @@ -67,10 +67,6 @@ "@fontsource/noto-sans": "^5.2.7", "@layerstack/svelte-stores": "^1.0.2", "@svelte-put/shortcut": "^4.1.0", - "@tanstack/query-async-storage-persister": "^5.90.12", - "@tanstack/svelte-query": "^6.0.8", - "@tanstack/svelte-query-devtools": "^6.0.0", - "@tanstack/svelte-query-persist-client": "^6.0.10", "@wuchale/svelte": "^0.17.5", "@wuchale/vite-plugin": "^0.15.3", "adze": "^2.2.4", diff --git a/web/src/lib/api/jail/jail.ts b/web/src/lib/api/jail/jail.ts index 162eeddc..e2f8402e 100644 --- a/web/src/lib/api/jail/jail.ts +++ b/web/src/lib/api/jail/jail.ts @@ -99,8 +99,8 @@ export async function getJailLogs(id: number): Promise { return await apiRequest(`/jail/${id}/logs`, JailLogsSchema, 'GET'); } -export async function getStats(ctId: number, limit: number): Promise { - return await apiRequest(`/jail/stats/${ctId}/${limit}`, z.array(JailStatSchema), 'GET'); +export async function getStats(ctId: number, step: string): Promise { + return await apiRequest(`/jail/stats/${ctId}/${step}`, z.array(JailStatSchema), 'GET'); } export async function addNetwork( diff --git a/web/src/lib/api/zfs/pool.ts b/web/src/lib/api/zfs/pool.ts index 547fb83f..ba140773 100644 --- a/web/src/lib/api/zfs/pool.ts +++ b/web/src/lib/api/zfs/pool.ts @@ -1,14 +1,10 @@ import { APIResponseSchema, type APIResponse } from '$lib/types/common'; import { - IODelayHistoricalSchema, - IODelaySchema, PoolsDiskUsageSchema, PoolStatPointsResponseSchema, ZpoolSchema, ZPoolStatusPoolSchema, type CreateZpool, - type IODelay, - type IODelayHistorical, type PoolsDiskUsage, type PoolStatPointsResponse, type ReplaceDevice, @@ -16,24 +12,6 @@ import { type ZpoolStatusPool } from '$lib/types/zfs/pool'; import { apiRequest } from '$lib/utils/http'; -import type { QueryFunctionContext } from '@tanstack/svelte-query'; - -export async function getIODelay( - queryObj: QueryFunctionContext | undefined -): Promise { - if (queryObj) { - if (queryObj.queryKey.includes('ioDelayHistorical')) { - const data = await apiRequest( - '/zfs/pool/io-delay/historical', - IODelayHistoricalSchema, - 'GET' - ); - return IODelayHistoricalSchema.parse(data); - } - } - - return await apiRequest('/zfs/pool/io-delay', IODelaySchema, 'GET'); -} export async function getPoolStatus(guid: string): Promise { return await apiRequest(`/zfs/pools/${guid}/status`, ZPoolStatusPoolSchema, 'GET'); diff --git a/web/src/lib/components/custom/Jail/Create/CreateJail.svelte b/web/src/lib/components/custom/Jail/Create/CreateJail.svelte index 65f7b358..8f44e80e 100644 --- a/web/src/lib/components/custom/Jail/Create/CreateJail.svelte +++ b/web/src/lib/components/custom/Jail/Create/CreateJail.svelte @@ -1,27 +1,19 @@ diff --git a/web/src/lib/utils/http.ts b/web/src/lib/utils/http.ts index dac8cf20..8076fcdc 100644 --- a/web/src/lib/utils/http.ts +++ b/web/src/lib/utils/http.ts @@ -106,14 +106,16 @@ export async function cachedFetch( const now = Date.now(); const entry = await kvStorage.getItem(key); - if (entry) { + console.log('cachedFetch:', { key, entry, now, duration }); + + if (entry && entry.data !== null) { const isFresh = now - entry.timestamp < duration; const data = entry.data; const looksLikeError = - data && typeof data === 'object' && - 'status' in (data as any) && + data !== null && + 'status' in data && (data as any).status === 'error'; if (isFresh && !looksLikeError) { diff --git a/web/src/lib/utils/vm/vm.ts b/web/src/lib/utils/vm/vm.ts index 8c80d7dd..c3d3b869 100644 --- a/web/src/lib/utils/vm/vm.ts +++ b/web/src/lib/utils/vm/vm.ts @@ -1,5 +1,5 @@ import type { Jail, SimpleJail } from '$lib/types/jail/jail'; -import type { CreateData, VM } from '$lib/types/vm/vm'; +import type { CreateData, SimpleVm, VM } from '$lib/types/vm/vm'; import { toast } from 'svelte-sonner'; import { isValidVMName } from '../string'; import type { UTypeGroupedDownload } from '$lib/types/utilities/downloader'; @@ -128,7 +128,7 @@ export function isValidCreateData( return true; } -export function getNextId(vms: VM[], jails: Jail[] | SimpleJail[]): number { +export function getNextId(vms: VM[] | SimpleVm[], jails: Jail[] | SimpleJail[]): number { const usedIds = [...vms.map((vm) => vm.rid), ...jails.map((jail) => jail.ctId)]; if (usedIds.length === 0) return 100; return Math.max(...usedIds) + 1; diff --git a/web/src/locales/en.po b/web/src/locales/en.po index 1b191022..015a38d7 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -32,7 +32,6 @@ msgstr "Console" #: src/lib/components/custom/VM/Create/CreateVM.svelte #: src/routes/[node]/+layout.svelte #: src/routes/[node]/+layout.svelte -#: src/routes/datacenter/+layout.svelte msgid "Storage" msgstr "Storage" @@ -116,9 +115,8 @@ msgstr "Explorer" msgid "Disks" msgstr "Disks" -#: src/routes/[node]/+layout.svelte -msgid "Dashboard" -msgstr "Dashboard" +#~ msgid "Dashboard" +#~ msgstr "Dashboard" #: src/routes/[node]/+layout.svelte #: src/routes/[node]/storage/zfs/dashboard/+page.svelte @@ -1055,12 +1053,14 @@ msgstr "Note" msgid "Post Upgrade Summary" msgstr "Post Upgrade Summary" +#: src/routes/[node]/notes/+page.svelte #: src/routes/[node]/notes/+page.svelte #: src/routes/datacenter/notes/+page.svelte #: src/routes/datacenter/notes/+page.svelte msgid "Content" msgstr "Content" +#: src/routes/[node]/notes/+page.svelte #: src/routes/datacenter/notes/+page.svelte msgid "This is a note" msgstr "This is a note" @@ -1107,8 +1107,6 @@ msgstr "Failed to delete notes" #: src/lib/components/custom/Charts/Area.svelte #: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/jail/[node]/summary/+page.svelte -#: src/routes/[node]/jail/[node]/summary/+page.svelte -#: src/routes/[node]/summary/+page.svelte #: src/routes/[node]/summary/+page.svelte #: src/routes/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte @@ -1171,7 +1169,7 @@ msgstr "Sylve Version" #: src/lib/components/custom/Charts/EChart.svelte #: src/lib/components/custom/Charts/EChartSample.svelte #: src/routes/[node]/jail/[node]/summary/+page.svelte -#: src/routes/[node]/jail/[node]/summary/+page.svelte +#: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Memory Usage" msgstr "Memory Usage" @@ -5395,22 +5393,27 @@ msgstr "Storage updated" #~ msgid "Manage ({0} lpinned)" #~ msgstr "Manage ({0} lpinned)" +#: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Hourly" msgstr "Hourly" +#: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Daily" msgstr "Daily" +#: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Weekly" msgstr "Weekly" +#: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Monthly" msgstr "Monthly" +#: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Yearly" msgstr "Yearly" @@ -6739,3 +6742,6 @@ msgstr "Manage the devfs ruleset for this jail" #: src/lib/components/custom/Jail/Options/TextEdit.svelte msgid "Manage additional options for this jail" msgstr "Manage additional options for this jail" + +#~ msgid "Discord" +#~ msgstr "Discord" diff --git a/web/src/locales/hi.po b/web/src/locales/hi.po index ffe3b024..49c8c145 100644 --- a/web/src/locales/hi.po +++ b/web/src/locales/hi.po @@ -32,7 +32,6 @@ msgstr "कंसोल" #: src/lib/components/custom/VM/Create/CreateVM.svelte #: src/routes/[node]/+layout.svelte #: src/routes/[node]/+layout.svelte -#: src/routes/datacenter/+layout.svelte msgid "Storage" msgstr "भंडारण" @@ -116,9 +115,8 @@ msgstr "एक्सप्लोरर" msgid "Disks" msgstr "डिस्क्स" -#: src/routes/[node]/+layout.svelte -msgid "Dashboard" -msgstr "डैशबोर्ड" +#~ msgid "Dashboard" +#~ msgstr "डैशबोर्ड" #: src/routes/[node]/+layout.svelte #: src/routes/[node]/storage/zfs/dashboard/+page.svelte @@ -1011,12 +1009,14 @@ msgstr "नोट" msgid "Post Upgrade Summary" msgstr "पोस्ट अपग्रेड सारांश" +#: src/routes/[node]/notes/+page.svelte #: src/routes/[node]/notes/+page.svelte #: src/routes/datacenter/notes/+page.svelte #: src/routes/datacenter/notes/+page.svelte msgid "Content" msgstr "सामग्री" +#: src/routes/[node]/notes/+page.svelte #: src/routes/datacenter/notes/+page.svelte msgid "This is a note" msgstr "यह एक नोट है" @@ -1105,8 +1105,6 @@ msgstr "क्लस्टर रीसेट के बाद लॉगिन #: src/lib/components/custom/Charts/Area.svelte #: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/jail/[node]/summary/+page.svelte -#: src/routes/[node]/jail/[node]/summary/+page.svelte -#: src/routes/[node]/summary/+page.svelte #: src/routes/[node]/summary/+page.svelte #: src/routes/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte @@ -1169,7 +1167,7 @@ msgstr "Sylve संस्करण" #: src/lib/components/custom/Charts/EChart.svelte #: src/lib/components/custom/Charts/EChartSample.svelte #: src/routes/[node]/jail/[node]/summary/+page.svelte -#: src/routes/[node]/jail/[node]/summary/+page.svelte +#: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Memory Usage" msgstr "मेमोरी उपयोग" @@ -5383,22 +5381,27 @@ msgstr "" #~ msgid "Manage ({0} lpinned)" #~ msgstr "" +#: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Hourly" msgstr "" +#: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Daily" msgstr "" +#: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Weekly" msgstr "" +#: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Monthly" msgstr "" +#: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Yearly" msgstr "" @@ -6699,3 +6702,6 @@ msgstr "" #: src/lib/components/custom/Jail/Options/TextEdit.svelte msgid "Manage additional options for this jail" msgstr "" + +#~ msgid "Discord" +#~ msgstr "" diff --git a/web/src/locales/mal.po b/web/src/locales/mal.po index 2a62c99e..86b1cdad 100644 --- a/web/src/locales/mal.po +++ b/web/src/locales/mal.po @@ -32,7 +32,6 @@ msgstr "" #: src/lib/components/custom/VM/Create/CreateVM.svelte #: src/routes/[node]/+layout.svelte #: src/routes/[node]/+layout.svelte -#: src/routes/datacenter/+layout.svelte msgid "Storage" msgstr "" @@ -116,9 +115,8 @@ msgstr "" msgid "Disks" msgstr "" -#: src/routes/[node]/+layout.svelte -msgid "Dashboard" -msgstr "" +#~ msgid "Dashboard" +#~ msgstr "" #: src/routes/[node]/+layout.svelte #: src/routes/[node]/storage/zfs/dashboard/+page.svelte @@ -1053,12 +1051,14 @@ msgstr "" msgid "Post Upgrade Summary" msgstr "" +#: src/routes/[node]/notes/+page.svelte #: src/routes/[node]/notes/+page.svelte #: src/routes/datacenter/notes/+page.svelte #: src/routes/datacenter/notes/+page.svelte msgid "Content" msgstr "" +#: src/routes/[node]/notes/+page.svelte #: src/routes/datacenter/notes/+page.svelte msgid "This is a note" msgstr "" @@ -1105,8 +1105,6 @@ msgstr "" #: src/lib/components/custom/Charts/Area.svelte #: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/jail/[node]/summary/+page.svelte -#: src/routes/[node]/jail/[node]/summary/+page.svelte -#: src/routes/[node]/summary/+page.svelte #: src/routes/[node]/summary/+page.svelte #: src/routes/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte @@ -1169,7 +1167,7 @@ msgstr "" #: src/lib/components/custom/Charts/EChart.svelte #: src/lib/components/custom/Charts/EChartSample.svelte #: src/routes/[node]/jail/[node]/summary/+page.svelte -#: src/routes/[node]/jail/[node]/summary/+page.svelte +#: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Memory Usage" msgstr "" @@ -5377,22 +5375,27 @@ msgstr "" #~ msgid "Manage ({0} lpinned)" #~ msgstr "" +#: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Hourly" msgstr "" +#: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Daily" msgstr "" +#: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Weekly" msgstr "" +#: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Monthly" msgstr "" +#: src/routes/[node]/jail/[node]/summary/+page.svelte #: src/routes/[node]/vm/[node]/summary/+page.svelte msgid "Yearly" msgstr "" @@ -6693,3 +6696,6 @@ msgstr "" #: src/lib/components/custom/Jail/Options/TextEdit.svelte msgid "Manage additional options for this jail" msgstr "" + +#~ msgid "Discord" +#~ msgstr "" diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index a187302e..a3524417 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -13,7 +13,6 @@ import { Toaster } from '$lib/components/ui/sonner/index.js'; import '$lib/utils/i18n'; import { addTabulatorFilters } from '$lib/utils/table'; - import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query'; import { ModeWatcher } from 'mode-watcher'; import { onMount } from 'svelte'; import '../locales/main.loader.svelte.js'; @@ -27,14 +26,6 @@ import Reboot from '$lib/components/custom/Initialization/Reboot.svelte'; import { getBasicSettings } from '$lib/api/system/settings.js'; - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - enabled: browser - } - } - }); - let { children } = $props(); let initialized = $state(null); let rebooted = $state(false); @@ -183,27 +174,25 @@ {#if loading.throbber} {:else if storage.hostname && storage.token && !loading.throbber && !loading.login} - - {#if initialized === null} - - {:else if initialized === false || rebooted === false} - {#if !initialized} -
- -
- {:else if !rebooted} -
- -
- {/if} - {:else} + {#if initialized === null} + + {:else if initialized === false || rebooted === false} + {#if !initialized}
- - {@render children()} - + +
+ {:else if !rebooted} +
+
{/if} -
+ {:else} +
+ + {@render children()} + +
+ {/if} {:else}
diff --git a/web/src/routes/[node]/+layout.svelte b/web/src/routes/[node]/+layout.svelte index d3380520..0ee6c673 100644 --- a/web/src/routes/[node]/+layout.svelte +++ b/web/src/routes/[node]/+layout.svelte @@ -128,11 +128,12 @@ label: 'ZFS', icon: 'file-icons--openzfs', children: [ - { - label: 'Dashboard', - icon: 'mdi--monitor-dashboard', - href: `/${node}/storage/zfs/dashboard` - }, + // Turned off dashboard for now + // { + // label: 'Dashboard', + // icon: 'mdi--monitor-dashboard', + // href: `/${node}/storage/zfs/dashboard` + // }, { label: 'Pools', icon: 'bi--hdd-stack-fill', href: `/${node}/storage/zfs/pools` }, { label: 'Datasets', @@ -272,7 +273,7 @@ -
- - - - - - - - - -
- - - -
-
- - - Dataset Compression -
-
- - -
-
-
-
- - - {#if histograms.compression.data.length === 0} -
-

No data available

-
- {:else} - - {/if} -
-
- - - - -
-
- - - Pool Usage -
- -
-
-
- - -
- -
-
-
-
- {/if} - diff --git a/web/src/routes/[node]/storage/zfs/dashboard/+page.ts b/web/src/routes/[node]/storage/zfs/dashboard/+page.ts index 7d83a1ad..e69de29b 100644 --- a/web/src/routes/[node]/storage/zfs/dashboard/+page.ts +++ b/web/src/routes/[node]/storage/zfs/dashboard/+page.ts @@ -1,19 +0,0 @@ -import { getDatasets } from '$lib/api/zfs/datasets'; -import { getPools, getPoolStats } from '$lib/api/zfs/pool'; -import { SEVEN_DAYS } from '$lib/utils'; -import { cachedFetch } from '$lib/utils/http'; - -export async function load() { - const cacheDuration = SEVEN_DAYS; - const [datasets, pools, poolStats] = await Promise.all([ - cachedFetch('datasets', async () => await getDatasets(), cacheDuration), - cachedFetch('pools', getPools, cacheDuration), - cachedFetch('pool-stats', async () => await getPoolStats(1, 128), cacheDuration) - ]); - - return { - pools: pools, - datasets: datasets, - poolStats: poolStats - }; -} diff --git a/web/src/routes/[node]/vm/[node]/summary/+page.svelte b/web/src/routes/[node]/vm/[node]/summary/+page.svelte index 6d17fcf4..fddbb183 100644 --- a/web/src/routes/[node]/vm/[node]/summary/+page.svelte +++ b/web/src/routes/[node]/vm/[node]/summary/+page.svelte @@ -25,12 +25,9 @@ import humanFormat from 'human-format'; import { toast } from 'svelte-sonner'; import { storage } from '$lib'; - import { resource, useInterval, Debounced, IsDocumentVisible } from 'runed'; - import { untrack } from 'svelte'; + import { resource, useInterval, Debounced, IsDocumentVisible, watch } from 'runed'; import type { GFSStep } from '$lib/types/common'; import SimpleSelect from '$lib/components/custom/SimpleSelect.svelte'; - import LayerBrush from '$lib/components/custom/Charts/LayerBrush.svelte'; - import EChart from '$lib/components/custom/Charts/EChartSample.svelte'; import LineBrush from '$lib/components/custom/Charts/LineBrush/Single.svelte'; interface Data { @@ -50,7 +47,7 @@ updateCache(key, result); return result; }, - { lazy: true, initialValue: data.vm } + { initialValue: data.vm } ); const domain = resource( @@ -60,7 +57,7 @@ updateCache(key, result); return result; }, - { lazy: true, initialValue: data.domain } + { initialValue: data.domain } ); const stats = resource( @@ -71,7 +68,7 @@ updateCache(key, result); return result; }, - { lazy: true, initialValue: data.stats } + { initialValue: data.stats } ); const visible = new IsDocumentVisible(); @@ -86,15 +83,16 @@ } }); - $effect(() => { - if (storage.visible) { - untrack(() => { + watch( + () => storage.idle, + (idle) => { + if (!idle) { vm.refetch(); domain.refetch(); stats.refetch(); - }); + } } - }); + ); let recentStat = $derived( stats.current[stats.current.length - 1] || getObjectSchemaDefaults(VMStatSchema) @@ -450,6 +448,17 @@ color="one" containerContentHeight="h-64" /> + + ({ + date: new Date(data.createdAt).getTime(), + value: Number(data.memoryUsage) + }))} + percentage={true} + color="two" + containerContentHeight="h-64" + /> diff --git a/web/src/routes/[node]/vm/[node]/summary/+page.ts b/web/src/routes/[node]/vm/[node]/summary/+page.ts index 3b2d7230..c73b9251 100644 --- a/web/src/routes/[node]/vm/[node]/summary/+page.ts +++ b/web/src/routes/[node]/vm/[node]/summary/+page.ts @@ -12,6 +12,8 @@ export async function load({ params }) { cachedFetch(`vm-stats-${rid}`, async () => getStats(Number(rid), 'hourly'), cacheDuration) ]); + console.log('VM Summary Load:', { rid, vm, domain, stats }); + return { rid: Number(rid), vm: vm, diff --git a/web/src/routes/datacenter/cluster/+page.svelte b/web/src/routes/datacenter/cluster/+page.svelte index e4835769..9f6c275f 100644 --- a/web/src/routes/datacenter/cluster/+page.svelte +++ b/web/src/routes/datacenter/cluster/+page.svelte @@ -13,9 +13,9 @@ import type { Column, Row } from '$lib/types/components/tree-table'; import { handleAPIError, updateCache } from '$lib/utils/http'; import { renderWithIcon } from '$lib/utils/table'; - import { createQuery } from '@tanstack/svelte-query'; import { toast } from 'svelte-sonner'; import type { CellComponent } from 'tabulator-tables'; + import { resource } from 'runed'; interface Data { cluster: ClusterDetails; @@ -23,34 +23,33 @@ let { data }: { data: Data } = $props(); let reload = $state(false); - const results = createQuery(() => ({ - queryKey: ['cluster-info'], - queryFn: async () => { - return await getDetails(); + + const datacenter = resource( + () => 'cluster-info', + async (key, prevKey, { signal }) => { + const res = await getDetails(); + updateCache('cluster-info', res); + return res; }, - keepPreviousData: true, - initialData: data.cluster, - refetchOnMount: 'always', - onSuccess: (data: ClusterDetails) => { - updateCache('cluster-info', data); - } - })); + { initialValue: data.cluster } + ); $effect(() => { if (reload) { - results.refetch(); + datacenter.refetch(); reload = false; } }); - let dataCenter = $derived(results.data); - let canReset = $derived(dataCenter?.cluster.enabled === true); + let canReset = $derived(datacenter.current.cluster.enabled === true); let canCreate = $derived( - dataCenter?.cluster.raftBootstrap === null && dataCenter?.cluster.enabled === false + datacenter.current.cluster.raftBootstrap === null && + datacenter.current.cluster.enabled === false ); let canJoin = $derived( - dataCenter?.cluster.raftBootstrap !== true && dataCenter?.cluster.enabled === false + datacenter.current.cluster.raftBootstrap !== true && + datacenter.current.cluster.enabled === false ); let modals = $state({ @@ -117,8 +116,8 @@ } ]; - if (dataCenter?.nodes) { - for (const node of dataCenter.nodes) { + if (datacenter.current.nodes) { + for (const node of datacenter.current.nodes) { rows.push({ id: node.id, leader: node.isLeader, @@ -199,7 +198,7 @@ - + ({ - queryKey: ['cluster-notes'], - queryFn: async () => { + let notes = resource( + () => 'cluster-notes', + async () => { return (await getNotes()) as Note[]; }, - refetchInterval: 1000, - keepPreviousData: true, - initialData: data.notes, - refetchOnMount: 'always' - })); + { + initialValue: data.notes + } + ); + + let reload = $state(false); + + watch( + () => reload, + (value) => { + if (value) { + notes.refetch(); + reload = false; + } + } + ); - let notes: Note[] = $derived(results.data as Note[]); let modalState = $state({ title: '', content: '', @@ -81,7 +91,7 @@ // } } else { const response = await createNote(modalState.title, modalState.content); - results.refetch(); + reload = true; if (response.status === 'success') { toast.success('Note created', { position: 'bottom-center' }); handleNote(undefined, false, true); @@ -97,7 +107,7 @@ } function viewNote(id: number | string | undefined) { - const note = notes.find((note) => note.id === id); + const note = notes.current.find((note) => note.id === id); if (note) { modalState.title = note.title; modalState.content = note.content; @@ -107,7 +117,7 @@ } function handleDelete(id: number | string | undefined) { - const note = notes.find((note) => note.id === id); + const note = notes.current.find((note) => note.id === id); if (note) { modalState.title = note.title; modalState.content = note.content; @@ -117,7 +127,7 @@ } function handleBulkDelete(ids: number[]) { - const notesToDelete = notes.filter((note) => ids.includes(note.id)); + const notesToDelete = notes.current.filter((note) => ids.includes(note.id)); if (notesToDelete.length > 0) { modalState.title = `${notesToDelete.length} notes`; modalState.isBulkDeleteOpen = true; @@ -153,7 +163,7 @@ } ]); - let tableData = $derived(generateTableData(columns, notes)); + let tableData = $derived(generateTableData(columns, notes.current)); let activeRow: Row[] | null = $state(null); let query: string = $state(''); @@ -168,8 +178,7 @@ class="h-6.5" >
- - + View
@@ -183,7 +192,7 @@ class="h-6.5" >
- + Delete
@@ -192,7 +201,7 @@ {#if type === 'edit-note'} @@ -219,7 +228,7 @@ class="h-6.5" >
- + Bulk Delete
@@ -233,7 +242,7 @@ @@ -249,8 +258,7 @@
- + {#if modalState.isEditMode} {#if selectedId} @@ -276,8 +284,7 @@ modalState.content = ''; }} > - - + {'Reset'}
@@ -354,8 +360,7 @@ onConfirm: async () => { const id = activeRow ? activeRow[0]?.id : null; const result = await deleteNote(id as number); - - results.refetch(); + reload = true; if (isAPIResponse(result) && result.status === 'success') { toast.success('Note deleted', { position: 'bottom-center' }); handleNote(undefined, false, true); @@ -381,7 +386,7 @@ ? activeRow.map((row) => (typeof row.id === 'number' ? row.id : parseInt(row.id))) : []; const result = await deleteNote(ids[0]); - results.refetch(); + reload = true; if (isAPIResponse(result) && result.status === 'success') { toast.success('Notes deleted', { position: 'bottom-center' }); handleNote(undefined, false, true); diff --git a/web/src/routes/datacenter/storage/+page.svelte b/web/src/routes/datacenter/storage/+page.svelte deleted file mode 100644 index 5ae13ece..00000000 --- a/web/src/routes/datacenter/storage/+page.svelte +++ /dev/null @@ -1,184 +0,0 @@ - - -{#snippet button(type: string)} - {#if activeRows && activeRows?.length !== 0} - {#if type === 'delete'} - - {/if} - {/if} -{/snippet} - -
-
- - - - - {@render button('delete')} -
- - -
- -{#if modals.create.open} - -{/if} - - { - let deleteFunc = modals.delete.type === 's3' ? deleteS3Storage : deleteS3Storage; - - const response = await deleteFunc(Number(activeRow?.id)); - reload = true; - if (response.error) { - handleAPIError(response); - toast.error(`Failed to delete ${activeRow?.name}`, { - position: 'bottom-center' - }); - return; - } - - toast.success(`Deleted ${activeRow?.name}`, { - position: 'bottom-center' - }); - - modals.delete.open = false; - }, - onCancel: () => { - modals.delete.open = false; - } - }} -> diff --git a/web/src/routes/datacenter/storage/+page.ts b/web/src/routes/datacenter/storage/+page.ts deleted file mode 100644 index 66534a7c..00000000 --- a/web/src/routes/datacenter/storage/+page.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { getNotes } from '$lib/api/cluster/notes'; -import { getStorages } from '$lib/api/cluster/storage'; -import type { Note } from '$lib/types/info/notes'; -import { SEVEN_DAYS } from '$lib/utils'; -import { cachedFetch } from '$lib/utils/http'; - -export async function load() { - const cacheDuration = SEVEN_DAYS; - const [storages] = await Promise.all([ - cachedFetch('cluster-storages', async () => getStorages(), cacheDuration) - ]); - - return { - storages: storages - }; -} diff --git a/web/src/routes/datacenter/summary/+page.svelte b/web/src/routes/datacenter/summary/+page.svelte index f748a454..7ff9ee57 100644 --- a/web/src/routes/datacenter/summary/+page.svelte +++ b/web/src/routes/datacenter/summary/+page.svelte @@ -2,21 +2,21 @@ import { getDetails, getNodes } from '$lib/api/cluster/cluster'; import { getCPUInfo } from '$lib/api/info/cpu'; import { getRAMInfo } from '$lib/api/info/ram'; - import { getPoolsDiskUsage, getPoolsDiskUsageFull } from '$lib/api/zfs/pool'; + import { getPoolsDiskUsageFull } from '$lib/api/zfs/pool'; import Arc from '$lib/components/custom/Charts/Arc.svelte'; import { Badge } from '$lib/components/ui/badge/index.js'; import * as Card from '$lib/components/ui/card/index.js'; import * as Table from '$lib/components/ui/table/index.js'; import type { ClusterDetails, ClusterNode } from '$lib/types/cluster/cluster'; - import type { CPUInfo, CPUInfoHistorical } from '$lib/types/info/cpu'; - import type { RAMInfo, RAMInfoHistorical } from '$lib/types/info/ram'; + import type { CPUInfo } from '$lib/types/info/cpu'; + import type { RAMInfo } from '$lib/types/info/ram'; import type { PoolsDiskUsage } from '$lib/types/zfs/pool'; import { getQuorumStatus } from '$lib/utils/cluster'; import { updateCache } from '$lib/utils/http'; import { capitalizeFirstLetter } from '$lib/utils/string'; import { dateToAgo } from '$lib/utils/time'; - import { createQueries } from '@tanstack/svelte-query'; import humanFormat from 'human-format'; + import { resource } from 'runed'; interface Data { nodes: ClusterNode[]; @@ -27,78 +27,73 @@ } let { data }: { data: Data } = $props(); - const results = createQueries(() => ({ - queries: [ - { - queryKey: ['cluster-nodes'], - queryFn: async () => { - return (await getNodes()) as ClusterNode[]; - }, - refetchInterval: 1000, - keepPreviousData: true, - initialData: data.nodes, - refetchOnMount: 'always', - onSuccess: (data: ClusterNode[]) => { - updateCache('cluster-nodes', data); - } - }, - { - queryKey: ['cluster-details'], - queryFn: async () => { - return (await getDetails()) as ClusterDetails; - }, - refetchInterval: 1000, - keepPreviousData: true, - initialData: data.details, - refetchOnMount: 'always', - onSuccess: (data: ClusterDetails) => { - updateCache('cluster-details', data); - } - }, - { - queryKey: ['cpu-info'], - queryFn: getCPUInfo, - keepPreviousData: true, - initialData: data.cpu, - refetchOnMount: 'always', - onSuccess: (data: CPUInfo | CPUInfoHistorical) => { - updateCache('cpu-info', data as CPUInfo); - } - }, - { - queryKey: ['ram-info'], - queryFn: getRAMInfo, - keepPreviousData: true, - initialData: data.ram, - onSuccess: (data: RAMInfo | RAMInfoHistorical) => { - updateCache('ram-info', data); - }, - refetchOnMount: true, - refetchOnWindowFocus: true - }, - { - queryKey: ['total-disk-usage'], - queryFn: getPoolsDiskUsageFull, - keepPreviousData: true, - initialData: data.disk, - onSuccess: (data: PoolsDiskUsage) => { - updateCache('total-disk-usage', data); - }, - refetchOnMount: true, - refetchOnWindowFocus: true - } - ] - })); - let nodes = $derived(results[0].data ?? []); - let clusterDetails = $derived(results[1].data); - let cpuInfo = $derived(results[2].data as CPUInfo); - let ramInfo = $derived(results[3].data as RAMInfo); - let diskInfo = $derived(results[4].data as PoolsDiskUsage); - let clustered = $derived(clusterDetails?.cluster.enabled || false); + let nodes = resource( + () => 'cluster-nodes', + async (key, prevKey, { signal }) => { + const result = await getNodes(); + updateCache('cluster-nodes', result); + return result; + }, + { + initialValue: data.nodes + } + ); + + let clusterDetails = resource( + () => 'cluster-details', + async (key, prevKey, { signal }) => { + const result = await getDetails(); + updateCache('cluster-details', result); + return result; + }, + { + initialValue: data.details + } + ); + + let cpuInfo = resource( + () => 'cpu-info', + async (key, prevKey, { signal }) => { + const result = await getCPUInfo('current'); + updateCache('cpu-info', result); + return result; + }, + { + initialValue: data.cpu + } + ); + + let ramInfo = resource( + () => 'ram-info', + async (key, prevKey, { signal }) => { + const result = await getRAMInfo('current'); + updateCache('ram-info', result); + return result; + }, + { + initialValue: data.ram + } + ); + + let diskInfo = resource( + () => 'total-disk-usage', + async (key, prevKey, { signal }) => { + const result = await getPoolsDiskUsageFull(); + updateCache('total-disk-usage', result); + return result; + }, + { + initialValue: data.disk + } + ); + + let clustered = $derived.by(() => { + return clusterDetails?.current.cluster.enabled ?? false; + }); let total = $derived.by(() => { - if (nodes.length === 0) { + if (nodes.current.length === 0) { return { cpu: { total: 0, usage: 0 }, ram: { total: 0, usage: 0 }, @@ -106,17 +101,20 @@ }; } - const totalCPUs = nodes.reduce((acc, node) => acc + node.cpu, 0); - const used = nodes.reduce((acc, node) => acc + (node.cpu * node.cpuUsage) / 100, 0); + const totalCPUs = nodes.current.reduce((acc, node) => acc + node.cpu, 0); + const used = nodes.current.reduce((acc, node) => acc + (node.cpu * node.cpuUsage) / 100, 0); - const totalMemory = nodes.reduce((acc, node) => acc + node.memory, 0); - const usedMemory = nodes.reduce( + const totalMemory = nodes.current.reduce((acc, node) => acc + node.memory, 0); + const usedMemory = nodes.current.reduce( (acc, node) => acc + ((node.memory ?? 0) * (node.memoryUsage ?? 0)) / 100, 0 ); - const totalDisk = nodes.reduce((acc, node) => acc + node.disk, 0); - const usedDisk = nodes.reduce((acc, node) => acc + (node.disk * node.diskUsage) / 100, 0); + const totalDisk = nodes.current.reduce((acc, node) => acc + node.disk, 0); + const usedDisk = nodes.current.reduce( + (acc, node) => acc + (node.disk * node.diskUsage) / 100, + 0 + ); return { cpu: { @@ -134,9 +132,9 @@ }; }); - let quorumStatus = $derived(getQuorumStatus(clusterDetails as ClusterDetails, nodes)); + let quorumStatus = $derived(getQuorumStatus(clusterDetails.current, nodes.current)); let statusCounts = $derived.by(() => { - return nodes.reduce( + return nodes.current.reduce( (acc, node) => { acc[node.status] = (acc[node.status] || 0) + 1; return acc; @@ -227,21 +225,25 @@ {:else} -
- -
+
+
+
{/if} @@ -274,7 +276,7 @@ - {#each nodes as node (node.id)} + {#each nodes.current as node (node.id)}