From c3fe936e05627527e9302baa5fac0203793933e9 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Tue, 26 May 2026 20:28:21 +0000 Subject: [PATCH 01/47] initial changes for API v2.0.0: - added access manager api token to auth object - update account page to show pool based resource quotas - update config logic to use pool based resource quotas - minor improvements and cleanup --- app/app.go | 2 +- app/common/types.go | 7 +- app/common/utils.go | 32 ++-- app/routes/account.go | 252 +++++++++++++-------------- app/routes/backups.go | 11 +- app/routes/config.go | 69 +++----- app/routes/index.go | 36 ++-- app/routes/login.go | 4 +- proxmoxaas-common-lib | 2 +- web/html/account.html | 33 +--- web/html/index.html | 4 +- web/html/login.html | 1 + web/scripts/backups.js | 4 +- web/scripts/clientsync.js | 2 +- web/scripts/config.js | 16 +- web/scripts/dialog.js | 2 +- web/scripts/draggable.js | 2 +- web/scripts/index.js | 100 +++++------ web/scripts/utils.js | 27 +-- web/templates/config.go.tmpl | 4 +- web/templates/pool-resources.go.tmpl | 34 ++++ 21 files changed, 309 insertions(+), 335 deletions(-) create mode 100644 web/templates/pool-resources.go.tmpl diff --git a/app/app.go b/app/app.go index 81d97b7..2c8d67c 100644 --- a/app/app.go +++ b/app/app.go @@ -38,7 +38,7 @@ func Run(configPath *string) { router.GET("/settings", routes.HandleGETSettings) // run on all interfaces with port - log.Fatal(router.Run(fmt.Sprintf("0.0.0.0:%d", common.Global.Port))) + log.Fatal("[Error] starting gin router: ", router.Run(fmt.Sprintf("0.0.0.0:%d", common.Global.Port))) } // setup static resources under web (css, images, modules, scripts) diff --git a/app/common/types.go b/app/common/types.go index f38ad5c..902681b 100644 --- a/app/common/types.go +++ b/app/common/types.go @@ -48,9 +48,10 @@ type RequestContext struct { } type Auth struct { - Username string - Token string - CSRF string + Username string + Token string + CSRF string + AccessManagerTicket string } type Icon struct { diff --git a/app/common/utils.go b/app/common/utils.go index 1561b3f..acbd91b 100644 --- a/app/common/utils.go +++ b/app/common/utils.go @@ -24,12 +24,12 @@ import ( func GetConfig(configPath string) Config { content, err := os.ReadFile(configPath) if err != nil { - log.Fatal("Error when opening config file: ", err) + log.Fatal("[Error] when opening config file: ", err) } var config Config err = json.Unmarshal(content, &config) if err != nil { - log.Fatal("Error during parsing config file: ", err) + log.Fatal("[Error] during parsing config file: ", err) } return config } @@ -54,7 +54,7 @@ func MinifyStatic(m *minify.M, files embed.FS) map[string]StaticFile { if !entry.IsDir() { v, err := files.ReadFile(path) if err != nil { - log.Fatalf("error parsing template file %s: %s", path, err.Error()) + log.Fatalf("[Error] parsing template file %s: %s", path, err.Error()) } x := strings.Split(entry.Name(), ".") if len(x) >= 2 { // file has extension @@ -62,7 +62,7 @@ func MinifyStatic(m *minify.M, files embed.FS) map[string]StaticFile { if ok && mimetype.Minifier != nil { // if the extension is mapped in MimeTypes and has a minifier min, err := m.String(mimetype.Type, string(v)) // try to minify if err != nil { - log.Fatalf("error minifying file %s: %s", path, err.Error()) + log.Fatalf("[Error] minifying file %s: %s", path, err.Error()) } minified[path] = StaticFile{ Data: min, @@ -185,7 +185,6 @@ func RequestGetAPI(path string, context RequestContext, body any) (*http.Respons if err != nil { return nil, response.StatusCode, err } - switch body.(type) { // write json to body object depending on type, currently supports map[string]any (ie json) or []any (ie array of json) case *map[string]any: err = json.Unmarshal(data, &body) @@ -208,10 +207,11 @@ func GetAuth(c *gin.Context) (Auth, error) { username, errUsername := c.Cookie("username") token, errToken := c.Cookie("PVEAuthCookie") csrf, errCSRF := c.Cookie("CSRFPreventionToken") - if errUsername != nil || errAuth != nil || errToken != nil || errCSRF != nil { + access, errAccess := c.Cookie("PAASAccessManagerTicket") + if errUsername != nil || errAuth != nil || errToken != nil || errCSRF != nil || errAccess != nil { return Auth{}, fmt.Errorf("error occured getting user cookies: (auth: %s, token: %s, csrf: %s)", errAuth, errToken, errCSRF) } else { - return Auth{username, token, csrf}, nil + return Auth{username, token, csrf, access}, nil } } @@ -239,13 +239,25 @@ func FormatNumber(val int64, base int64) (float64, string) { steps++ } - if base == 1000 { + switch base { + case 1000: prefixes := []string{"", "K", "M", "G", "T"} return valf, prefixes[steps] - } else if base == 1024 { + case 1024: prefixes := []string{"", "Ki", "Mi", "Gi", "Ti"} return valf, prefixes[steps] - } else { + default: return 0, "" } } + +func GetRequestContextFromCookies(auth Auth) RequestContext { + return RequestContext{ + Cookies: map[string]string{ + "username": auth.Username, + "PVEAuthCookie": auth.Token, + "CSRFPreventionToken": auth.CSRF, + "PAASAccessManagerTicket": auth.AccessManagerTicket, + }, + } +} diff --git a/app/routes/account.go b/app/routes/account.go index 499d125..f024c23 100644 --- a/app/routes/account.go +++ b/app/routes/account.go @@ -3,6 +3,7 @@ package routes import ( "fmt" "net/http" + paas "proxmoxaas-common-lib" "proxmoxaas-dashboard/app/common" "github.com/gerow/go-color" @@ -12,13 +13,7 @@ import ( type Account struct { Username string - Pools map[string]bool - Nodes map[string]bool - VMID struct { - Min int - Max int - } - Resources map[string]map[string]any + Pools map[string]paas.Pool } // numerical constraint @@ -103,171 +98,168 @@ var Green = color.RGB{ func HandleGETAccount(c *gin.Context) { auth, err := common.GetAuth(c) if err == nil { - account, err := GetUserAccount(auth) + pools, err := GetUserPools(auth) if err != nil { common.HandleNonFatalError(c, err) return } - // for each resource category, create a resource chart - for category, resources := range account.Resources { - for resource, v := range resources { - switch t := v.(type) { - case NumericResource: - avail, prefix := common.FormatNumber(t.Total.Avail*t.Multiplier, t.Base) - account.Resources[category][resource] = ResourceChart{ - Type: t.Type, - Display: t.Display, - Name: t.Name, - Used: t.Total.Used, - Max: t.Total.Max, - Avail: avail, - Prefix: prefix, - Unit: t.Unit, - ColorHex: InterpolateColorHSV(Green, Red, float64(t.Total.Used)/float64(t.Total.Max)).ToHTML(), - } - case StorageResource: - avail, prefix := common.FormatNumber(t.Total.Avail*t.Multiplier, t.Base) - account.Resources[category][resource] = ResourceChart{ - Type: t.Type, - Display: t.Display, - Name: t.Name, - Used: t.Total.Used, - Max: t.Total.Max, - Avail: avail, - Prefix: prefix, - Unit: t.Unit, - ColorHex: InterpolateColorHSV(Green, Red, float64(t.Total.Used)/float64(t.Total.Max)).ToHTML(), - } - case ListResource: - l := struct { - Type string - Display bool - Resources []ResourceChart - }{ - Type: t.Type, - Display: t.Display, - Resources: []ResourceChart{}, - } - - for _, r := range t.Total { - l.Resources = append(l.Resources, ResourceChart{ + for poolname, pool := range pools { + // for each resource category + for category := range pool.Resources { + // for each resource in each category + for resource, v := range pool.Resources[category].(map[string]any) { + // create a resource chart for resource depending on resource type + switch t := v.(type) { + case NumericResource: + avail, prefix := common.FormatNumber(t.Total.Avail*t.Multiplier, t.Base) + pools[poolname].Resources[category].(map[string]any)[resource] = ResourceChart{ Type: t.Type, Display: t.Display, - Name: r.Name, - Used: r.Used, - Max: r.Max, - Avail: float64(r.Avail), // usually an int - Unit: "", - ColorHex: InterpolateColorHSV(Green, Red, float64(r.Used)/float64(r.Max)).ToHTML(), - }) + Name: t.Name, + Used: t.Total.Used, + Max: t.Total.Max, + Avail: avail, + Prefix: prefix, + Unit: t.Unit, + ColorHex: InterpolateColorHSV(Green, Red, float64(t.Total.Used)/float64(t.Total.Max)).ToHTML(), + } + case StorageResource: + avail, prefix := common.FormatNumber(t.Total.Avail*t.Multiplier, t.Base) + pools[poolname].Resources[category].(map[string]any)[resource] = ResourceChart{ + Type: t.Type, + Display: t.Display, + Name: t.Name, + Used: t.Total.Used, + Max: t.Total.Max, + Avail: avail, + Prefix: prefix, + Unit: t.Unit, + ColorHex: InterpolateColorHSV(Green, Red, float64(t.Total.Used)/float64(t.Total.Max)).ToHTML(), + } + case ListResource: + l := struct { + Type string + Display bool + Resources []ResourceChart + }{ + Type: t.Type, + Display: t.Display, + Resources: []ResourceChart{}, + } + + for _, r := range t.Total { + l.Resources = append(l.Resources, ResourceChart{ + Type: t.Type, + Display: t.Display, + Name: r.Name, + Used: r.Used, + Max: r.Max, + Avail: float64(r.Avail), // usually an int + Unit: "", + ColorHex: InterpolateColorHSV(Green, Red, float64(r.Used)/float64(r.Max)).ToHTML(), + }) + } + pools[poolname].Resources[category].(map[string]any)[resource] = l } - account.Resources[category][resource] = l } } } c.HTML(http.StatusOK, "html/account.html", gin.H{ - "global": common.Global, - "page": "account", - "account": account, + "global": common.Global, + "page": "account", + "account": map[string]any{ + "Username": auth.Username, + "Pools": pools, + }, }) } else { c.Redirect(http.StatusFound, "/login") // if user is not authed, redirect user to login page } } -func GetUserAccount(auth common.Auth) (Account, error) { - account := Account{ - Resources: map[string]map[string]any{}, - } +func GetUserPools(auth common.Auth) (map[string]paas.Pool, error) { + pools := map[string]paas.Pool{} - ctx := common.RequestContext{ - Cookies: map[string]string{ - "username": auth.Username, - "PVEAuthCookie": auth.Token, - "CSRFPreventionToken": auth.CSRF, - }, - } - - // get user account basic data + // get all pools + ctx := common.GetRequestContextFromCookies(auth) body := map[string]any{} - res, code, err := common.RequestGetAPI("/user/config/cluster", ctx, &body) + res, code, err := common.RequestGetAPI("/access/pools", ctx, &body) if err != nil { - return account, err + return pools, err } if code != 200 { - return account, fmt.Errorf("request to /user/config/cluster resulted in %+v", res) + return pools, fmt.Errorf("request to /access/pools resulted in %+v", res) } - err = mapstructure.Decode(body, &account) + err = mapstructure.Decode(body["pools"].(map[string]any), &pools) if err != nil { - return account, err - } else { - account.Username = auth.Username + return pools, err } - body = map[string]any{} - // get user resources - res, code, err = common.RequestGetAPI("/user/dynamic/resources", ctx, &body) - if err != nil { - return account, err - } - if code != 200 { - return account, fmt.Errorf("request to /user/dynamic/resources resulted in %+v", res) - } - resources := body - + // get global config for resource type metadata body = map[string]any{} // get resource meta data res, code, err = common.RequestGetAPI("/global/config/resources", ctx, &body) if err != nil { - return account, err + return pools, err } if code != 200 { - return account, fmt.Errorf("request to /global/config/resources resulted in %+v", res) + return pools, fmt.Errorf("request to /global/config/resources resulted in %+v", res) } meta := body["resources"].(map[string]any) - // build each resource by its meta type - for k, v := range meta { - m := v.(map[string]any) - t := m["type"].(string) - r := resources[k].(map[string]any) - category := m["category"].(string) - if _, ok := account.Resources[category]; !ok { - account.Resources[category] = map[string]any{} - } - if t == "numeric" { - n := NumericResource{} - n.Type = t - err_m := mapstructure.Decode(m, &n) - err_r := mapstructure.Decode(r, &n) - if err_m != nil || err_r != nil { - return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error()) + // for each pool + for poolname, pool := range pools { + // for each resource in pool data + for k, v := range pool.Resources { + m := meta[k].(map[string]any) + t := m["type"].(string) + r := v.(map[string]any) + category := m["category"].(string) + + // create a category if it does not already exist + if _, ok := pool.Resources[category]; !ok { + pool.Resources[category] = map[string]any{} } - account.Resources[category][k] = n - } else if t == "storage" { - n := StorageResource{} - n.Type = t - err_m := mapstructure.Decode(m, &n) - err_r := mapstructure.Decode(r, &n) - if err_m != nil || err_r != nil { - return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error()) + + // depending on type, decode the pool data into the corresponding resource type + switch t { + case "numeric": + n := NumericResource{} + n.Type = t + err_m := mapstructure.Decode(m, &n) + err_r := mapstructure.Decode(r, &n) + if err_m != nil || err_r != nil { + return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error()) + } + pools[poolname].Resources[category].(map[string]any)[k] = n + case "storage": + n := StorageResource{} + n.Type = t + err_m := mapstructure.Decode(m, &n) + err_r := mapstructure.Decode(r, &n) + if err_m != nil || err_r != nil { + return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error()) + } + pools[poolname].Resources[category].(map[string]any)[k] = n + case "list": + n := ListResource{} + n.Type = t + err_m := mapstructure.Decode(m, &n) + err_r := mapstructure.Decode(r, &n) + if err_m != nil || err_r != nil { + return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error()) + } + pools[poolname].Resources[category].(map[string]any)[k] = n } - account.Resources[category][k] = n - } else if t == "list" { - n := ListResource{} - n.Type = t - err_m := mapstructure.Decode(m, &n) - err_r := mapstructure.Decode(r, &n) - if err_m != nil || err_r != nil { - return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error()) - } - account.Resources[category][k] = n + + // delete the old entry, only categories should be left at the end of the loop + delete(pools[poolname].Resources, k) } } - return account, nil + return pools, nil } // interpolate between min and max by normalized (0 - 1) val diff --git a/app/routes/backups.go b/app/routes/backups.go index 93aadd6..944d299 100644 --- a/app/routes/backups.go +++ b/app/routes/backups.go @@ -2,7 +2,6 @@ package routes import ( "fmt" - "log" "net/http" "proxmoxaas-dashboard/app/common" "time" @@ -39,8 +38,6 @@ func HandleGETBackups(c *gin.Context) { common.HandleNonFatalError(c, fmt.Errorf("error encountered getting instance config: %s", err.Error())) } - log.Printf("%+v", backups) - c.HTML(http.StatusOK, "html/backups.html", gin.H{ "global": common.Global, "page": "backups", @@ -79,13 +76,7 @@ func HandleGETBackupsFragment(c *gin.Context) { func GetInstanceBackups(vm common.VMPath, auth common.Auth) ([]InstanceBackup, error) { backups := []InstanceBackup{} path := fmt.Sprintf("/cluster/%s/%s/%s/backup", vm.Node, vm.Type, vm.VMID) - ctx := common.RequestContext{ - Cookies: map[string]string{ - "username": auth.Username, - "PVEAuthCookie": auth.Token, - "CSRFPreventionToken": auth.CSRF, - }, - } + ctx := common.GetRequestContextFromCookies(auth) body := []any{} res, code, err := common.RequestGetAPI(path, ctx, &body) if err != nil { diff --git a/app/routes/config.go b/app/routes/config.go index fcb6536..c43d6a3 100644 --- a/app/routes/config.go +++ b/app/routes/config.go @@ -15,16 +15,7 @@ import ( // imported types from fabric type InstanceConfig struct { - Type paas.InstanceType `json:"type"` - Name string `json:"name"` - CPU string `json:"cpu"` - Cores uint64 `json:"cores"` - Memory uint64 `json:"memory"` - Swap uint64 `json:"swap"` - Volumes map[string]*paas.Volume `json:"volumes"` - Nets map[string]*paas.Net `json:"nets"` - Devices map[string]*paas.Device `json:"devices"` - Boot paas.BootOrder `json:"boot"` + paas.Instance `mapstructure:",squash"` // overrides ProctypeSelect common.Select } @@ -35,17 +26,13 @@ type GlobalConfig struct { } } -type UserConfigResources struct { +type PoolConfig struct { CPU struct { - Global []CPUConfig - Nodes map[string][]CPUConfig + Global []paas.MatchLimit + Nodes map[string][]paas.MatchLimit } } -type CPUConfig struct { - Name string -} - func HandleGETConfig(c *gin.Context) { auth, err := common.GetAuth(c) if err == nil { @@ -61,13 +48,13 @@ func HandleGETConfig(c *gin.Context) { } if config.Type == "VM" { // if VM, fetch CPU types from node - config.ProctypeSelect, err = GetCPUTypes(vm_path, auth) + config.ProctypeSelect, err = GetCPUTypes(vm_path, config.Pool, auth) if err != nil { common.HandleNonFatalError(c, fmt.Errorf("error encountered getting proctypes: %s", err.Error())) } } for i, cpu := range config.ProctypeSelect.Options { - if cpu.Value == config.CPU { + if cpu.Value == config.Proctype { config.ProctypeSelect.Options[i].Selected = true } } @@ -181,13 +168,7 @@ func HandleGETConfigBootFragment(c *gin.Context) { func GetInstanceConfig(vm common.VMPath, auth common.Auth) (InstanceConfig, error) { config := InstanceConfig{} path := fmt.Sprintf("/cluster/%s/%s/%s", vm.Node, vm.Type, vm.VMID) - ctx := common.RequestContext{ - Cookies: map[string]string{ - "username": auth.Username, - "PVEAuthCookie": auth.Token, - "CSRFPreventionToken": auth.CSRF, - }, - } + ctx := common.GetRequestContextFromCookies(auth) body := map[string]any{} res, code, err := common.RequestGetAPI(path, ctx, &body) if err != nil { @@ -208,20 +189,14 @@ func GetInstanceConfig(vm common.VMPath, auth common.Auth) (InstanceConfig, erro return config, nil } -func GetCPUTypes(vm common.VMPath, auth common.Auth) (common.Select, error) { +func GetCPUTypes(vm common.VMPath, pool string, auth common.Auth) (common.Select, error) { cputypes := common.Select{ ID: "proctype", Required: true, } // get global resource config - ctx := common.RequestContext{ - Cookies: map[string]string{ - "username": auth.Username, - "PVEAuthCookie": auth.Token, - "CSRFPreventionToken": auth.CSRF, - }, - } + ctx := common.GetRequestContextFromCookies(auth) body := map[string]any{} path := "/global/config/resources" res, code, err := common.RequestGetAPI(path, ctx, &body) @@ -231,15 +206,15 @@ func GetCPUTypes(vm common.VMPath, auth common.Auth) (common.Select, error) { if code != 200 { return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res) } - global := GlobalConfig{} - err = mapstructure.Decode(body["resources"], &global) + globalConfig := GlobalConfig{} + err = mapstructure.Decode(body["resources"], &globalConfig) if err != nil { return cputypes, err } - // get user resource config + // get pool resource config body = map[string]any{} - path = "/user/config/resources" + path = fmt.Sprintf("/access/pools/%s", pool) res, code, err = common.RequestGetAPI(path, ctx, &body) if err != nil { return cputypes, err @@ -247,21 +222,21 @@ func GetCPUTypes(vm common.VMPath, auth common.Auth) (common.Select, error) { if code != 200 { return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res) } - user := UserConfigResources{} - err = mapstructure.Decode(body, &user) + poolCPUConfig := PoolConfig{} + err = mapstructure.Decode(body["pool"].(map[string]any)["resources"], &poolCPUConfig) if err != nil { return cputypes, err } // use node specific rules if present, otherwise use global rules - var userCPU []CPUConfig - if _, ok := user.CPU.Nodes[vm.Node]; ok { - userCPU = user.CPU.Nodes[vm.Node] + var userCPU []paas.MatchLimit + if _, ok := poolCPUConfig.CPU.Nodes[vm.Node]; ok { + userCPU = poolCPUConfig.CPU.Nodes[vm.Node] } else { - userCPU = user.CPU.Global + userCPU = poolCPUConfig.CPU.Global } - if global.CPU.Whitelist { // cpu is a whitelist + if globalConfig.CPU.Whitelist { // cpu is a whitelist for _, cpu := range userCPU { // for each cpu type in user config add it to the options cputypes.Options = append(cputypes.Options, common.Option{ Display: cpu.Name, @@ -280,7 +255,7 @@ func GetCPUTypes(vm common.VMPath, auth common.Auth) (common.Select, error) { return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res) } supported := struct { - data []CPUConfig + data []paas.MatchLimit }{} err = mapstructure.Decode(body, supported) if err != nil { @@ -289,7 +264,7 @@ func GetCPUTypes(vm common.VMPath, auth common.Auth) (common.Select, error) { // for each node supported cpu type, if it is NOT in the user's config (aka is not blacklisted) then add it to the options for _, cpu := range supported.data { - contains := slices.ContainsFunc(userCPU, func(c CPUConfig) bool { + contains := slices.ContainsFunc(userCPU, func(c paas.MatchLimit) bool { return c.Name == cpu.Name }) if !contains { diff --git a/app/routes/index.go b/app/routes/index.go index f37b5ba..94e27e1 100644 --- a/app/routes/index.go +++ b/app/routes/index.go @@ -83,13 +83,8 @@ func HandleGETInstancesFragment(c *gin.Context) { } func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]Node, error) { - ctx := common.RequestContext{ - Cookies: map[string]string{ - "PVEAuthCookie": auth.Token, - "CSRFPreventionToken": auth.CSRF, - }, - } - body := map[string]any{} + ctx := common.GetRequestContextFromCookies(auth) + body := []any{} res, code, err := common.RequestGetAPI("/proxmox/cluster/resources", ctx, &body) if err != nil { return nil, nil, err @@ -102,16 +97,17 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No nodes := map[string]Node{} // parse /proxmox/cluster/resources to separate instances and nodes - for _, v := range body["data"].([]any) { + for _, v := range body { m := v.(map[string]any) - if m["type"] == "node" { // if type is node -> parse as Node object + switch m["type"] { + case "node": // if type is node -> parse as Node object node := Node{} err := mapstructure.Decode(v, &node) if err != nil { return nil, nil, err } nodes[node.Node] = node - } else if m["type"] == "lxc" || m["type"] == "qemu" { // if type is lxc or qemu -> parse as InstanceCard object + case "lxc", "qemu": // if type is lxc or qemu -> parse as InstanceCard object instance := InstanceCard{} err := mapstructure.Decode(v, &instance) if err != nil { @@ -127,9 +123,10 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No // set instance's config link path instance.ConfigPath = fmt.Sprintf("config?node=%s&type=%s&vmid=%d", instance.Node, instance.Type, instance.VMID) // set the instance's console link path - if instance.Type == "qemu" { + switch instance.Type { + case "qemu": instance.ConsolePath = fmt.Sprintf("%s/?console=kvm&vmid=%d&vmname=%s&node=%s&resize=off&cmd=&novnc=1", common.Global.PVE, instance.VMID, instance.Name, instance.Node) - } else if instance.Type == "lxc" { + case "lxc": instance.ConsolePath = fmt.Sprintf("%s/?console=lxc&vmid=%d&vmname=%s&node=%s&resize=off&cmd=&xtermjs=1", common.Global.PVE, instance.VMID, instance.Name, instance.Node) } // set the instance's backups link path @@ -138,7 +135,7 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No instances[vmid] = instance } - body = map[string]any{} + body = []any{} res, code, err = common.RequestGetAPI("/proxmox/cluster/tasks", ctx, &body) if err != nil { return nil, nil, err @@ -151,7 +148,7 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No expected_state := map[uint]string{} // iterate through recent user accessible tasks to find the task most recently made on an instance - for _, v := range body["data"].([]any) { + for _, v := range body { // parse task as Task object task := Task{} err := mapstructure.Decode(v, &task) @@ -179,10 +176,11 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No continue } else { // recent task is a start or stop task for user instance which is running or "OK" if task.EndTime > most_recent_task[task.VMID] { // if the task's end time is later than the most recent one encountered - most_recent_task[task.VMID] = task.EndTime // update the most recent task - if task.Type == "qmstart" || task.Type == "vzstart" { // if the task was a start task, update the expected state to running + most_recent_task[task.VMID] = task.EndTime // update the most recent task + switch task.Type { + case "qmstart", "vzstart": // if the task was a start task, update the expected state to running expected_state[task.VMID] = "running" - } else if task.Type == "qmstop" || task.Type == "vzstop" { // if the task was a stop task, update the expected state to stopped + case "qmstop", "vzstop": // if the task was a stop task, update the expected state to stopped expected_state[task.VMID] = "stopped" } } @@ -195,7 +193,7 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No // get /status/current which is updated faster than /cluster/resources instance := instances[vmid] path := fmt.Sprintf("/proxmox/nodes/%s/%s/%d/status/current", instance.Node, instance.Type, instance.VMID) - body = map[string]any{} + body := map[string]any{} res, code, err := common.RequestGetAPI(path, ctx, &body) if err != nil { return nil, nil, err @@ -205,7 +203,7 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No } status := InstanceStatus{} - mapstructure.Decode(body["data"], &status) + mapstructure.Decode(body, &status) instance.Status = status.Status instances[vmid] = instance diff --git a/app/routes/login.go b/app/routes/login.go index f7a3c3a..29ecd86 100644 --- a/app/routes/login.go +++ b/app/routes/login.go @@ -27,7 +27,7 @@ func GetLoginRealms() ([]Realm, error) { ctx := common.RequestContext{ Cookies: nil, } - body := map[string]any{} + body := []any{} res, code, err := common.RequestGetAPI("/proxmox/access/domains", ctx, &body) if err != nil { return realms, err @@ -36,7 +36,7 @@ func GetLoginRealms() ([]Realm, error) { return realms, fmt.Errorf("request to /proxmox/access/domains resulted in %+v", res) } - for _, v := range body["data"].([]any) { + for _, v := range body { v = v.(map[string]any) realm := Realm{} err := mapstructure.Decode(v, &realm) diff --git a/proxmoxaas-common-lib b/proxmoxaas-common-lib index cc53d7b..52ac2c2 160000 --- a/proxmoxaas-common-lib +++ b/proxmoxaas-common-lib @@ -1 +1 @@ -Subproject commit cc53d7bdea6ead7ceba9bfd0c25e41392601094b +Subproject commit 52ac2c2b97050ef476005a2294e4433f10559735 diff --git a/web/html/account.html b/web/html/account.html index e531bb1..d15c14c 100644 --- a/web/html/account.html +++ b/web/html/account.html @@ -42,9 +42,6 @@

Account Details

Username: {{.account.Username}}

-

Pools: {{MapKeys .account.Pools ", "}}

-

VMID Range: {{.account.VMID.Min}} - {{.account.VMID.Max}}

-

Nodes: {{MapKeys .account.Nodes ", "}}

@@ -52,33 +49,9 @@
-
-

Cluster Resources

-
- {{range $category, $v := .account.Resources}} - {{if ne $category ""}} -

{{$category}}

- {{end}} -
- {{range $v}} - {{if .Display}} - {{if eq .Type "numeric"}} - {{template "resource-chart" .}} - {{end}} - {{if eq .Type "storage"}} - {{template "resource-chart" .}} - {{end}} - {{if eq .Type "list"}} - {{range .Resources}} - {{template "resource-chart" .}} - {{end}} - {{end}} - {{end}} - {{end}} -
- {{end}} -
-
+ {{range $poolname, $pool := .account.Pools}} + {{template "pool-resources" $pool}} + {{end}} - + diff --git a/web/templates/select.go.tmpl b/web/templates/select.go.tmpl index aa9d046..32ed5b7 100644 --- a/web/templates/select.go.tmpl +++ b/web/templates/select.go.tmpl @@ -1,11 +1,27 @@ +{{/* + Select: generic data driven -{{range .Options}} + {{range .Options}} + {{template "option" .}} + {{end}} + +{{end}} + +{{/* + Options: generic data driven {{else}} {{end}} -{{end}} - {{end}} \ No newline at end of file From d88a208da597ae4678eda1b4de67e9a681bed7c4 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Fri, 5 Jun 2026 21:38:51 +0000 Subject: [PATCH 11/47] font consistency fixes --- app/routes/account.go | 37 +++++++++++++++++++++++++++++-------- web/css/form.css | 1 + web/css/nav.css | 3 +-- web/css/style.css | 18 +++++++++++++++++- web/html/account.html | 10 +++------- web/html/index.html | 2 +- web/scripts/clientsync.js | 2 +- 7 files changed, 53 insertions(+), 20 deletions(-) diff --git a/app/routes/account.go b/app/routes/account.go index 9f3e986..8144325 100644 --- a/app/routes/account.go +++ b/app/routes/account.go @@ -12,8 +12,8 @@ import ( ) type Account struct { - Username string - Pools map[string]paas.Pool + paas.User + Pools map[string]paas.Pool } // numerical constraint @@ -98,6 +98,13 @@ var Green = color.RGB{ func HandleGETAccount(c *gin.Context) { auth, err := common.GetAuth(c) if err == nil { + + account, err := GetUser(auth) + if err != nil { + common.HandleNonFatalError(c, err) + return + } + pools, err := GetUserPools(auth) if err != nil { common.HandleNonFatalError(c, err) @@ -167,19 +174,33 @@ func HandleGETAccount(c *gin.Context) { } } + account.Pools = pools + c.HTML(http.StatusOK, "html/account.html", gin.H{ - "global": common.Global, - "page": "account", - "account": map[string]any{ - "Username": auth.Username, - "Pools": pools, - }, + "global": common.Global, + "page": "account", + "account": account, }) } else { c.Redirect(http.StatusFound, "/login") // if user is not authed, redirect user to login page } } +func GetUser(auth common.Auth) (Account, error) { + account := Account{} + ctx := common.GetRequestContextFromCookies(auth) + body := map[string]any{} + res, code, err := common.RequestGetAPI(fmt.Sprintf("/access/users/%s", auth.Username), ctx, &body) + if err != nil { + return account, err + } + if code != 200 { + return account, fmt.Errorf("request to /access/pools resulted in %+v", res) + } + err = mapstructure.Decode(body, &account) + return account, err +} + func GetUserPools(auth common.Auth) (map[string]paas.Pool, error) { pools := map[string]paas.Pool{} diff --git a/web/css/form.css b/web/css/form.css index 447b545..95a5e1b 100644 --- a/web/css/form.css +++ b/web/css/form.css @@ -41,6 +41,7 @@ legend { fieldset { border: 0; + padding: 0; } fieldset > *:last-child { diff --git a/web/css/nav.css b/web/css/nav.css index 155beb3..7ffe358 100644 --- a/web/css/nav.css +++ b/web/css/nav.css @@ -53,7 +53,6 @@ header { } header h1 { - font-size: 18px; margin: 0; background-color: var(--nav-header-bg-color); color: var(--nav-header-text-color); @@ -61,8 +60,8 @@ header h1 { } nav { + font-size: var(--small-font-size); overflow: hidden; - font-size: larger; width: fit-content; } diff --git a/web/css/style.css b/web/css/style.css index 1f5b860..0420bf2 100644 --- a/web/css/style.css +++ b/web/css/style.css @@ -3,6 +3,9 @@ --positive-color: #0f0; --highlight-color: yellow; --lightbg-text-color: black; + --large-font-size: 32px; + --medium-font-size: 24px; + --small-font-size: 16px; } @media screen and (prefers-color-scheme: dark) { @@ -41,9 +44,22 @@ } } -* { +*, h1, h2, h3, p { box-sizing: border-box; font-family: monospace; + +} + +h1, p { + font-size: var(--small-font-size); +} + +h2 { + font-size: var(--large-font-size); +} + +h3 { + font-size: var(--medium-font-size); } html { diff --git a/web/html/account.html b/web/html/account.html index 5b26aa0..0674a8e 100644 --- a/web/html/account.html +++ b/web/html/account.html @@ -39,13 +39,9 @@

Account

Account Details

-

Username: {{.account.Username}}

-
-
-
-

Password

- -
+

Username: {{.account.Username.UserID}}@{{.account.Username.Realm}}

+

Email: {{.account.Mail}}

+

Password:

{{range $poolname, $pool := .account.Pools}} {{template "pool-resources" $pool}} diff --git a/web/html/index.html b/web/html/index.html index 052b6c2..f3cbd02 100644 --- a/web/html/index.html +++ b/web/html/index.html @@ -72,7 +72,7 @@
diff --git a/web/scripts/clientsync.js b/web/scripts/clientsync.js index 2b6c347..43994d7 100644 --- a/web/scripts/clientsync.js +++ b/web/scripts/clientsync.js @@ -3,7 +3,7 @@ import { getSyncSettings, requestAPI } from "./utils.js"; export async function setupClientSync (callback) { const { scheme, rate } = getSyncSettings(); if (scheme === "never") { - return + return; } else if (scheme === "always") { window.setInterval(callback, rate * 1000); From 94233000df81ad72f7fb371ee9c2730b086f580b Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Sat, 6 Jun 2026 23:11:51 +0000 Subject: [PATCH 12/47] fix search button --- web/html/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/html/index.html b/web/html/index.html index f3cbd02..3374632 100644 --- a/web/html/index.html +++ b/web/html/index.html @@ -72,7 +72,7 @@
From 00fa5f315238ef554948350f87d1f9f9b0449eaf Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Mon, 8 Jun 2026 18:34:18 +0000 Subject: [PATCH 13/47] fix incorrect display in backups --- app/routes/backups.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/backups.go b/app/routes/backups.go index 481822a..97d620a 100644 --- a/app/routes/backups.go +++ b/app/routes/backups.go @@ -97,7 +97,7 @@ func GetInstanceBackups(vm common.VMPath, auth common.Auth) ([]InstanceBackup, e for i := range backups { size, prefix := common.FormatNumber(backups[i].Size, 1024) - backups[i].SizeFormatted = fmt.Sprintf("%.3g %sB", size, prefix) + backups[i].SizeFormatted = fmt.Sprintf("%s %sB", size, prefix) t := time.Unix(backups[i].CTime, 0) backups[i].TimeFormatted = t.Format("02-01-06 15:04:05") From 10ef24e76b090f4adaef488fe816484ce8dc3019 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Mon, 8 Jun 2026 20:28:39 +0000 Subject: [PATCH 14/47] move some util definitions and code to common lib --- app/common/types.go | 18 --------------- app/common/utils.go | 50 +++++++++++++++++++++++++----------------- app/routes/account.go | 14 +++++------- app/routes/backups.go | 16 +++++++------- app/routes/config.go | 42 +++++++++++++++++------------------ app/routes/index.go | 14 ++++++------ app/routes/login.go | 5 +---- app/routes/settings.go | 2 +- proxmoxaas-common-lib | 2 +- 9 files changed, 74 insertions(+), 89 deletions(-) diff --git a/app/common/types.go b/app/common/types.go index 902681b..d75a330 100644 --- a/app/common/types.go +++ b/app/common/types.go @@ -22,13 +22,6 @@ type StaticFile struct { MimeType MimeType } -// parsed vmpath data (ie node/type/vmid) -type VMPath struct { - Node string - Type string - VMID string -} - // type used for templated + - +
diff --git a/web/html/index.html b/web/html/index.html index af363b0..6e3c88e 100644 --- a/web/html/index.html +++ b/web/html/index.html @@ -114,9 +114,9 @@ - + - +
From ce1576fbd6759addba67f4acc1ea5d859c34fc57 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Wed, 29 Jul 2026 16:13:55 +0000 Subject: [PATCH 40/47] remove global usage for wasm --- WFA-JS | 2 +- web/modules/wfa.js | 2 +- web/scripts/index.js | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/WFA-JS b/WFA-JS index f8f8636..f53f344 160000 --- a/WFA-JS +++ b/WFA-JS @@ -1 +1 @@ -Subproject commit f8f8636cc3f00e3fa0c379b490b7df05b9df3d2c +Subproject commit f53f344fb35dc118d55a2135123ddf64801d0cd3 diff --git a/web/modules/wfa.js b/web/modules/wfa.js index f99b95d..67bae2c 100644 --- a/web/modules/wfa.js +++ b/web/modules/wfa.js @@ -560,7 +560,7 @@ export default function init (path) { const wasm = obj.instance; global.wfa = wasm go.run(wasm); - res() + res(wasm) } if ('instantiateStreaming' in WebAssembly) { WebAssembly.instantiateStreaming(fetch(path), go.importObject).then(function (obj) { diff --git a/web/scripts/index.js b/web/scripts/index.js index 2c58403..a989c11 100644 --- a/web/scripts/index.js +++ b/web/scripts/index.js @@ -5,10 +5,12 @@ import wfaInit from "../modules/wfa.js"; window.addEventListener("DOMContentLoaded", init); +var wfa; + async function init () { setAppearance(); - wfaInit("modules/wfa.wasm"); + wfa = await wfaInit("modules/wfa.wasm"); initInstances(); document.querySelector("#instance-add").addEventListener("click", handleInstanceAddButton); @@ -272,8 +274,8 @@ function sortInstances () { }; criteria = (item, query) => { // lower is better - const { score, CIGAR } = global.wfa.wfAlign(query, item, penalties, true); - const alignment = global.wfa.DecodeCIGAR(CIGAR); + const { score, CIGAR } = wfa.wfAlign(query, item, penalties, true); + const alignment = wfa.DecodeCIGAR(CIGAR); return { score: score / item.length, alignment }; }; } From e8fd120eade3d0bb1361e55e8db99ecaa16fd67b Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Wed, 29 Jul 2026 17:49:52 +0000 Subject: [PATCH 41/47] add static analysis action --- .gitea/workflows/static_go.yaml | 33 +++++++++++++++++++ .../workflows/{lint.yaml => style_web.yaml} | 33 ++++++------------- Makefile | 12 ++++--- 3 files changed, 51 insertions(+), 27 deletions(-) create mode 100644 .gitea/workflows/static_go.yaml rename .gitea/workflows/{lint.yaml => style_web.yaml} (59%) diff --git a/.gitea/workflows/static_go.yaml b/.gitea/workflows/static_go.yaml new file mode 100644 index 0000000..0c641c5 --- /dev/null +++ b/.gitea/workflows/static_go.yaml @@ -0,0 +1,33 @@ +name: Static Analysis -- Golang +run-name: 'Static Analysis -- Golang -- ${{ gitea.repository }} ${{ gitea.ref_name }} ${{ gitea.sha }}' +on: + push: + branches: + - '*' + +jobs: + lint-check: + runs-on: ubuntu-latest + steps: + - name: Check Out Repository + uses: actions/checkout@v4 + with: + submodules: 'true' + + - name: Initialize Runner + uses: actions/setup-go@v7 + with: + go-version-file: 'go.mod' + + - name: Initialize Go + run: make build-web && go get . + + - name: Run Go Vet + id: static_govet + if: always() + run: go vet ./... + + - name: Verify Static Analysis + if: > + steps.static_govet.outcome != 'success' + run: exit 1 \ No newline at end of file diff --git a/.gitea/workflows/lint.yaml b/.gitea/workflows/style_web.yaml similarity index 59% rename from .gitea/workflows/lint.yaml rename to .gitea/workflows/style_web.yaml index 5d1306d..0316ed6 100644 --- a/.gitea/workflows/lint.yaml +++ b/.gitea/workflows/style_web.yaml @@ -1,9 +1,9 @@ -name: ProxmoxAAS-Dashboard Code Style Check -run-name: Code style check on ${{ gitea.repository }} ${{ gitea.ref_name }} ${{ gitea.sha }} +name: Code Style -- Web Components +run-name: 'Code Style -- Web Components -- ${{ gitea.repository }} ${{ gitea.ref_name }} ${{ gitea.sha }}' on: push: branches: - - "*" + - '*' jobs: lint-check: @@ -34,12 +34,12 @@ jobs: - name: Run HTMLValidate id: html_lint - run: npx html-validate --config dev_config/.htmlvalidate.json "web/html/**/*" + run: npx html-validate --config dev_config/.htmlvalidate.json 'web/html/**/*' - name: Run Stylelint id: style_lint if: always() - run: npx stylelint --config dev_config/.stylelintrc.json --formatter verbose --fix "web/css/**/*.css" + run: npx stylelint --config dev_config/.stylelintrc.json --formatter verbose --fix 'web/css/**/*.css' - name: Run ESLint id: js_lint @@ -49,21 +49,8 @@ jobs: run: npx eslint --config dev_config/eslint.config.mjs --fix web/scripts/ - name: Verify Linters - if: always() - run: | - FAILED=0 - if [ "${{ steps.html_lint.outcome }}" != "success" ]; then - echo "HTML Linter failed" - FAILED=1 - fi - if [ "${{ steps.style_lint.outcome }}" != "success" ]; then - echo "Style Linter failed" - FAILED=1 - fi - if [ "${{ steps.js_lint.outcome }}" != "success" ]; then - echo "JS Linter failed" - FAILED=1 - fi - if [ $FAILED -eq 1 ]; then - exit 1 - fi \ No newline at end of file + if: > + steps.html_lint.outcome != 'success' || + steps.style_lint.outcome != 'success' || + steps.js_lint.outcome != 'success' + run: exit 1 \ No newline at end of file diff --git a/Makefile b/Makefile index 1dcf62c..8f95981 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,17 @@ .PHONY: build test clean wfa-js -build: clean build-wfa-js +build: clean ensure-dist build-web build-wfa-js @echo "======================== Building Binary =======================" - mkdir -p dist # resolve symbolic links in web by copying it into dist/web/ - cp -rL web/ dist/web/ CGO_ENABLED=0 go build -tags release -ldflags="-s -w" -v -o dist/ . -build-wfa-js: +ensure-dist: + mkdir -p dist + +build-web: ensure-dist + cp -rL web/ dist/web/ + +build-wfa-js: ensure-dist $(MAKE) -C WFA-JS cp -f WFA-JS/dist/* web/modules From 613f01456f9dec6ef3677268288c166441895ac0 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Wed, 29 Jul 2026 23:19:25 +0000 Subject: [PATCH 42/47] use central repo for common workflows --- .gitea/workflows/static_go.yaml | 33 ------------------- .gitea/workflows/style_web.yaml | 56 --------------------------------- 2 files changed, 89 deletions(-) delete mode 100644 .gitea/workflows/static_go.yaml delete mode 100644 .gitea/workflows/style_web.yaml diff --git a/.gitea/workflows/static_go.yaml b/.gitea/workflows/static_go.yaml deleted file mode 100644 index 0c641c5..0000000 --- a/.gitea/workflows/static_go.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: Static Analysis -- Golang -run-name: 'Static Analysis -- Golang -- ${{ gitea.repository }} ${{ gitea.ref_name }} ${{ gitea.sha }}' -on: - push: - branches: - - '*' - -jobs: - lint-check: - runs-on: ubuntu-latest - steps: - - name: Check Out Repository - uses: actions/checkout@v4 - with: - submodules: 'true' - - - name: Initialize Runner - uses: actions/setup-go@v7 - with: - go-version-file: 'go.mod' - - - name: Initialize Go - run: make build-web && go get . - - - name: Run Go Vet - id: static_govet - if: always() - run: go vet ./... - - - name: Verify Static Analysis - if: > - steps.static_govet.outcome != 'success' - run: exit 1 \ No newline at end of file diff --git a/.gitea/workflows/style_web.yaml b/.gitea/workflows/style_web.yaml deleted file mode 100644 index 0316ed6..0000000 --- a/.gitea/workflows/style_web.yaml +++ /dev/null @@ -1,56 +0,0 @@ -name: Code Style -- Web Components -run-name: 'Code Style -- Web Components -- ${{ gitea.repository }} ${{ gitea.ref_name }} ${{ gitea.sha }}' -on: - push: - branches: - - '*' - -jobs: - lint-check: - runs-on: ubuntu-latest - steps: - - name: Check Out Repository - uses: actions/checkout@v4 - - - name: Initialize Runner - uses: actions/setup-node@v4 - - - name: Install Linters - run: | - npm install --no-save \ - @eslint/eslintrc \ - @eslint/js \ - eslint \ - globals \ - html-validate \ - stylelint \ - stylelint-config-standard - - - name: Verify Linters - run: | - npx html-validate --version - npx stylelint --version - npx eslint --version - - - name: Run HTMLValidate - id: html_lint - run: npx html-validate --config dev_config/.htmlvalidate.json 'web/html/**/*' - - - name: Run Stylelint - id: style_lint - if: always() - run: npx stylelint --config dev_config/.stylelintrc.json --formatter verbose --fix 'web/css/**/*.css' - - - name: Run ESLint - id: js_lint - if: always() - env: - DEBUG: eslint:cli-engine - run: npx eslint --config dev_config/eslint.config.mjs --fix web/scripts/ - - - name: Verify Linters - if: > - steps.html_lint.outcome != 'success' || - steps.style_lint.outcome != 'success' || - steps.js_lint.outcome != 'success' - run: exit 1 \ No newline at end of file From ffa58e421814376d58912231463350dd403ba44f Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Thu, 30 Jul 2026 18:58:53 +0000 Subject: [PATCH 43/47] use replace for web instead of static import --- Makefile | 8 +++++--- app/app.go | 2 +- go.mod | 2 ++ web/go.mod | 3 +++ 4 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 web/go.mod diff --git a/Makefile b/Makefile index 8f95981..3e3d076 100644 --- a/Makefile +++ b/Makefile @@ -5,9 +5,6 @@ build: clean ensure-dist build-web build-wfa-js # resolve symbolic links in web by copying it into dist/web/ CGO_ENABLED=0 go build -tags release -ldflags="-s -w" -v -o dist/ . -ensure-dist: - mkdir -p dist - build-web: ensure-dist cp -rL web/ dist/web/ @@ -22,3 +19,8 @@ clean: clean-wfa-js clean-wfa-js: $(MAKE) clean -C WFA-JS + +ensure-dist: + mkdir -p dist + +workflow-init: ensure-dist build-web diff --git a/app/app.go b/app/app.go index 8643ce8..aabd32e 100644 --- a/app/app.go +++ b/app/app.go @@ -6,7 +6,7 @@ import ( "log" "proxmoxaas-dashboard/app/common" "proxmoxaas-dashboard/app/routes" - "proxmoxaas-dashboard/dist/web" // go will complain here until the first build + "web" // go will complain here until the first build "github.com/gin-gonic/gin" "github.com/tdewolff/minify/v2" diff --git a/go.mod b/go.mod index 8a2290b..699a964 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,11 @@ require ( github.com/go-viper/mapstructure/v2 v2.5.0 github.com/tdewolff/minify/v2 v2.24.13 proxmoxaas-common-lib v0.0.0 + web v0.0.0 ) replace proxmoxaas-common-lib => ./proxmoxaas-common-lib +replace web => ./dist/web require ( github.com/bytedance/gopkg v0.1.4 // indirect diff --git a/web/go.mod b/web/go.mod new file mode 100644 index 0000000..32b9c93 --- /dev/null +++ b/web/go.mod @@ -0,0 +1,3 @@ +module web + +go 1.26.0 From ff63d8dc150f151e29bea9ebd9b7f9f5a6df14a2 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Fri, 31 Jul 2026 23:20:17 +0000 Subject: [PATCH 44/47] prevent error dialog from clearing on closing --- web/scripts/dialog.js | 2 +- web/scripts/login.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/web/scripts/dialog.js b/web/scripts/dialog.js index f430acb..b634c91 100644 --- a/web/scripts/dialog.js +++ b/web/scripts/dialog.js @@ -96,7 +96,7 @@ class ErrorDialog extends HTMLElement { } navigator.clipboard.writeText(errors); } - this.parentElement.removeChild(this); + this.dialog.close(); }); } diff --git a/web/scripts/login.js b/web/scripts/login.js index a6b779b..afa57c2 100644 --- a/web/scripts/login.js +++ b/web/scripts/login.js @@ -6,6 +6,7 @@ window.addEventListener("DOMContentLoaded", init); async function init () { await deleteAllCookies(); setAppearance(); + const formSubmitButton = document.querySelector("#submit"); formSubmitButton.addEventListener("click", async (e) => { e.preventDefault(); From 91be26e3203da931dbf5083106f5746cdf1bc68b Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Fri, 31 Jul 2026 23:30:32 +0000 Subject: [PATCH 45/47] update gitignore, format eslint config --- .gitignore | 2 -- dev_config/eslint.config.mjs | 51 +++++++++++++++++++----------------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 0b57df7..85caad2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ **/config.json -**/package-lock.json -**/node_modules dist/* go.sum \ No newline at end of file diff --git a/dev_config/eslint.config.mjs b/dev_config/eslint.config.mjs index cc7db54..2c4095d 100644 --- a/dev_config/eslint.config.mjs +++ b/dev_config/eslint.config.mjs @@ -2,28 +2,31 @@ import { defineConfig } from "eslint/config"; import globals from "globals"; import js from "@eslint/js"; -export default defineConfig([js.configs.recommended,{ - languageOptions: { - globals: { - ...globals.browser, +export default defineConfig([ + js.configs.recommended, + { + languageOptions: { + globals: { + ...globals.browser, + }, + ecmaVersion: "latest", + sourceType: "module", }, - ecmaVersion: "latest", - sourceType: "module", - }, - rules: { - "no-tabs": ["error", { - allowIndentationTabs: true, - }], - indent: ["error", "tab"], - "linebreak-style": ["error", "unix"], - quotes: ["error", "double"], - semi: ["error", "always"], - "brace-style": ["error", "stroustrup", { allowSingleLine: false }], - "no-unused-vars": ["warn", { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_" - }], - "prefer-const": ["error"] - }, -}]); \ No newline at end of file + rules: { + "no-tabs": ["error", { + allowIndentationTabs: true, + }], + indent: ["error", "tab"], + "linebreak-style": ["error", "unix"], + quotes: ["error", "double"], + semi: ["error", "always"], + "brace-style": ["error", "stroustrup", { allowSingleLine: false }], + "no-unused-vars": ["warn", { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + }], + "prefer-const": ["error"] + }, + } +]); \ No newline at end of file From 075ea07766e80067afac20e15fba83ae9cbe50e5 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Fri, 31 Jul 2026 23:37:51 +0000 Subject: [PATCH 46/47] add ingore paths for web component linters --- Makefile | 1 + dev_config/.htmlvalidateignore | 3 +++ dev_config/.stylelintignore | 3 +++ dev_config/eslint.config.mjs | 3 ++- 4 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 dev_config/.htmlvalidateignore create mode 100644 dev_config/.stylelintignore diff --git a/Makefile b/Makefile index 3e3d076..ccf8822 100644 --- a/Makefile +++ b/Makefile @@ -24,3 +24,4 @@ ensure-dist: mkdir -p dist workflow-init: ensure-dist build-web + cp -r dev_config/. . \ No newline at end of file diff --git a/dev_config/.htmlvalidateignore b/dev_config/.htmlvalidateignore new file mode 100644 index 0000000..7f0cc41 --- /dev/null +++ b/dev_config/.htmlvalidateignore @@ -0,0 +1,3 @@ +web/modules/* +WFA-JS/* +dist/* \ No newline at end of file diff --git a/dev_config/.stylelintignore b/dev_config/.stylelintignore new file mode 100644 index 0000000..7f0cc41 --- /dev/null +++ b/dev_config/.stylelintignore @@ -0,0 +1,3 @@ +web/modules/* +WFA-JS/* +dist/* \ No newline at end of file diff --git a/dev_config/eslint.config.mjs b/dev_config/eslint.config.mjs index 2c4095d..63a55e2 100644 --- a/dev_config/eslint.config.mjs +++ b/dev_config/eslint.config.mjs @@ -1,9 +1,10 @@ -import { defineConfig } from "eslint/config"; +import { defineConfig, globalIgnores } from "eslint/config"; import globals from "globals"; import js from "@eslint/js"; export default defineConfig([ js.configs.recommended, + globalIgnores(["dist/", "web/modules", "WFA-JS"]), { languageOptions: { globals: { From cbed40d904933c06212b053ea122e249c873bba0 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Mon, 3 Aug 2026 18:24:18 +0000 Subject: [PATCH 47/47] fix makefile phony targets --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ccf8822..2f66934 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build test clean wfa-js +.PHONY: build build-web build-wfa-js clean clean-wfa-js ensure-dist workflow-init build: clean ensure-dist build-web build-wfa-js @echo "======================== Building Binary ======================="