1 Commits
Author SHA1 Message Date
alu a8e311db12 cleanup settings logic 2026-07-14 22:13:23 +00:00
31 changed files with 618 additions and 584 deletions

No files matched your search

+2
View File
@@ -1,3 +1,5 @@
**/config.json **/config.json
**/package-lock.json
**/node_modules
dist/* dist/*
go.sum go.sum
+12 -18
View File
@@ -1,26 +1,20 @@
.PHONY: build build-web build-wfa clean clean-wfa clean-web .PHONY: build test clean wfa-js
build: clean ensure-dist build-wfa build-web build: clean wfa-js
@echo "======================== Building Binary =======================" @echo "======================== Building Binary ======================="
mkdir -p dist
# resolve symbolic links in web by copying it into dist/web/ # 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/ . CGO_ENABLED=0 go build -tags release -ldflags="-s -w" -v -o dist/ .
clean: clean-wfa clean-web wfa-js:
@echo "======================== Cleaning Project ======================"
go clean
rm -rf dist/*
ensure-dist:
mkdir -p dist
build-web: ensure-dist
cp -rL web/ dist/web/
clean-web:
build-wfa:
$(MAKE) -C WFA-JS $(MAKE) -C WFA-JS
cp -f WFA-JS/dist/* web/modules cp -f WFA-JS/dist/* web/modules
clean-wfa: test: clean
$(MAKE) clean -C WFA-JS go run .
clean:
@echo "======================== Cleaning Project ======================"
go clean
rm -rf dist/*
+1 -1
View File
@@ -13,7 +13,7 @@ ProxmoxAAS Dashboard provides users of a proxmox based compute on demand service
## ProxmoxAAS System Installation Overview ## ProxmoxAAS System Installation Overview
The ProxmoxAAS project is large and is split into multiple components. There are four required components, the Dashboard, API, Fabric, and access-manager-api. The installation order should start with the Dashboard and then proceed to the other backend components. This will require some foresight into the setup process. The ProxmoxAAS project is large and is split into multiple components. There are four required components, the Dashboard, API, Fabric, and access-manager-api. The instalation order should start with the Dashboard and then proceed to the other backend components. This will require some foresight into the setup process.
The supported setup is to use a reverse proxy to serve both the original Proxmox web interface and ProxmoxAAS components. It is possible other setups can work. Rather than provide specific steps to duplicate a certain setup, the steps included are intended as a guideline of steps required for proper function in most setups. Consequently, the examples provided are only to highlight key settings and do not represent complete working configurations. The instructions also assume you have your own domain name which will substitute `domain.net` in some of the configs. The supported setup is to use a reverse proxy to serve both the original Proxmox web interface and ProxmoxAAS components. It is possible other setups can work. Rather than provide specific steps to duplicate a certain setup, the steps included are intended as a guideline of steps required for proper function in most setups. Consequently, the examples provided are only to highlight key settings and do not represent complete working configurations. The instructions also assume you have your own domain name which will substitute `domain.net` in some of the configs.
+1 -1
Submodule WFA-JS updated: b0aacd5f9c...f8f8636cc3.
+2 -8
View File
@@ -6,7 +6,7 @@ import (
"log" "log"
"proxmoxaas-dashboard/app/common" "proxmoxaas-dashboard/app/common"
"proxmoxaas-dashboard/app/routes" "proxmoxaas-dashboard/app/routes"
"web" // go will complain here until the first build "proxmoxaas-dashboard/dist/web" // go will complain here until the first build
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/tdewolff/minify/v2" "github.com/tdewolff/minify/v2"
@@ -18,11 +18,9 @@ func Run() {
common.Global = common.GetConfig(*configPath) common.Global = common.GetConfig(*configPath)
// setup gin // setup static resources
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
router := gin.Default() router := gin.Default()
// setup static resources
m := common.InitMinify() m := common.InitMinify()
ServeStatic(router, m) ServeStatic(router, m)
html := common.MinifyStatic(m, web.Templates) html := common.MinifyStatic(m, web.Templates)
@@ -53,28 +51,24 @@ func ServeStatic(router *gin.Engine, m *minify.M) {
router.GET("/css/*css", func(c *gin.Context) { router.GET("/css/*css", func(c *gin.Context) {
path, _ := c.Params.Get("css") path, _ := c.Params.Get("css")
data := css[fmt.Sprintf("css%s", path)] data := css[fmt.Sprintf("css%s", path)]
c.Header("Content-Encoding", data.MimeType.Encoding)
c.Data(200, data.MimeType.Type, []byte(data.Data)) c.Data(200, data.MimeType.Type, []byte(data.Data))
}) })
images := common.MinifyStatic(m, web.Images_fs) images := common.MinifyStatic(m, web.Images_fs)
router.GET("/images/*image", func(c *gin.Context) { router.GET("/images/*image", func(c *gin.Context) {
path, _ := c.Params.Get("image") path, _ := c.Params.Get("image")
data := images[fmt.Sprintf("images%s", path)] data := images[fmt.Sprintf("images%s", path)]
c.Header("Content-Encoding", data.MimeType.Encoding)
c.Data(200, data.MimeType.Type, []byte(data.Data)) c.Data(200, data.MimeType.Type, []byte(data.Data))
}) })
modules := common.MinifyStatic(m, web.Modules_fs) modules := common.MinifyStatic(m, web.Modules_fs)
router.GET("/modules/*module", func(c *gin.Context) { router.GET("/modules/*module", func(c *gin.Context) {
path, _ := c.Params.Get("module") path, _ := c.Params.Get("module")
data := modules[fmt.Sprintf("modules%s", path)] data := modules[fmt.Sprintf("modules%s", path)]
c.Header("Content-Encoding", data.MimeType.Encoding)
c.Data(200, data.MimeType.Type, []byte(data.Data)) c.Data(200, data.MimeType.Type, []byte(data.Data))
}) })
scripts := common.MinifyStatic(m, web.Scripts_fs) scripts := common.MinifyStatic(m, web.Scripts_fs)
router.GET("/scripts/*script", func(c *gin.Context) { router.GET("/scripts/*script", func(c *gin.Context) {
path, _ := c.Params.Get("script") path, _ := c.Params.Get("script")
data := scripts[fmt.Sprintf("scripts%s", path)] data := scripts[fmt.Sprintf("scripts%s", path)]
c.Header("Content-Encoding", data.MimeType.Encoding)
c.Data(200, data.MimeType.Type, []byte(data.Data)) c.Data(200, data.MimeType.Type, []byte(data.Data))
}) })
} }
+4 -6
View File
@@ -11,7 +11,6 @@ import (
// defines mime type and associated minifier // defines mime type and associated minifier
type MimeType struct { type MimeType struct {
Type string Type string
Encoding string
Minifier func(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error Minifier func(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error
} }
@@ -37,6 +36,10 @@ var MimeTypes = map[string]MimeType{
Type: "image/svg+xml", Type: "image/svg+xml",
Minifier: nil, Minifier: nil,
}, },
"png": {
Type: "image/png",
Minifier: nil,
},
"js": { "js": {
Type: "application/javascript", Type: "application/javascript",
Minifier: nil, Minifier: nil,
@@ -45,11 +48,6 @@ var MimeTypes = map[string]MimeType{
Type: "application/wasm", Type: "application/wasm",
Minifier: nil, Minifier: nil,
}, },
"gz": {
Type: "application/wasm",
Encoding: "gzip",
Minifier: nil,
},
"*": { "*": {
Type: "text/plain", Type: "text/plain",
Minifier: nil, Minifier: nil,
+4 -6
View File
@@ -15,7 +15,6 @@ import (
// defines mime type and associated minifier // defines mime type and associated minifier
type MimeType struct { type MimeType struct {
Type string Type string
Encoding string
Minifier func(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error Minifier func(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error
} }
@@ -41,6 +40,10 @@ var MimeTypes = map[string]MimeType{
Type: "image/svg+xml", Type: "image/svg+xml",
Minifier: svg.Minify, Minifier: svg.Minify,
}, },
"png": {
Type: "image/png",
Minifier: nil,
},
"js": { "js": {
Type: "application/javascript", Type: "application/javascript",
Minifier: js.Minify, Minifier: js.Minify,
@@ -49,11 +52,6 @@ var MimeTypes = map[string]MimeType{
Type: "application/wasm", Type: "application/wasm",
Minifier: nil, Minifier: nil,
}, },
"gz": {
Type: "application/wasm",
Encoding: "gzip",
Minifier: nil,
},
"*": { "*": {
Type: "text/plain", Type: "text/plain",
Minifier: nil, Minifier: nil,
+144 -101
View File
@@ -11,9 +11,64 @@ import (
"github.com/go-viper/mapstructure/v2" "github.com/go-viper/mapstructure/v2"
) )
type Pool struct { type Account struct {
paas.Pool `mapstructure:",squash"` paas.User
ResourceCharts map[string]map[string]any Pools map[string]paas.Pool
}
// numerical constraint
type Constraint struct {
Max int64
Used int64
Avail int64
}
// match constraint
type Match struct {
Name string
Match string
Max int64
Used int64
Avail int64
}
type NumericResource struct {
Type string
Name string
Multiplier int64
Base uint64
Compact bool
Unit string
Display bool
Global Constraint
Nodes map[string]Constraint
Total Constraint
Category string
}
type StorageResource struct {
Type string
Name string
Multiplier int64
Base uint64
Compact bool
Unit string
Display bool
Disks []string
Global Constraint
Nodes map[string]Constraint
Total Constraint
Category string
}
type ListResource struct {
Type string
Whitelist bool
Display bool
Global []Match
Nodes map[string][]Match
Total []Match
Category string
} }
type ResourceChart struct { type ResourceChart struct {
@@ -43,7 +98,8 @@ var Green = color.RGB{
func HandleGETAccount(c *gin.Context) { func HandleGETAccount(c *gin.Context) {
auth, err := common.GetAuthFromRequest(c) auth, err := common.GetAuthFromRequest(c)
if err == nil { if err == nil {
user, err := GetUserBasic(auth)
account, err := GetUser(auth)
if err != nil { if err != nil {
common.HandleNonFatalError(c, err) common.HandleNonFatalError(c, err)
return return
@@ -55,33 +111,97 @@ func HandleGETAccount(c *gin.Context) {
return return
} }
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 := paas.FormatNumber(paas.SafeUint64(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 StorageResource:
avail, prefix := paas.FormatNumber(paas.SafeUint64(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 {
avail := fmt.Sprintf("%d", r.Avail)
l.Resources = append(l.Resources, ResourceChart{
Type: t.Type,
Display: t.Display,
Name: r.Name,
Used: r.Used,
Max: r.Max,
Avail: 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.Pools = pools
c.HTML(http.StatusOK, "html/account.html", gin.H{ c.HTML(http.StatusOK, "html/account.html", gin.H{
"global": common.Global, "global": common.Global,
"page": "account", "page": "account",
"user": user, "account": account,
"pools": pools,
}) })
} else { } else {
c.Redirect(http.StatusFound, "/login") // if user is not authed, redirect user to login page c.Redirect(http.StatusFound, "/login") // if user is not authed, redirect user to login page
} }
} }
func GetUserBasic(auth paas.Auth) (paas.User, error) { func GetUser(auth paas.Auth) (Account, error) {
user := paas.User{} account := Account{}
body := map[string]any{} body := map[string]any{}
res, code, err := common.RequestGetAPI(fmt.Sprintf("/access/users/%s", auth.Username), &auth, &body) res, code, err := common.RequestGetAPI(fmt.Sprintf("/access/users/%s", auth.Username), &auth, &body)
if err != nil { if err != nil {
return user, err return account, err
} }
if code != 200 { if code != 200 {
return user, fmt.Errorf("request to /access/pools resulted in %+v", res) return account, fmt.Errorf("request to /access/pools resulted in %+v", res)
} }
err = mapstructure.Decode(body["user"], &user) err = mapstructure.Decode(body, &account)
return user, err return account, err
} }
func GetUserPools(auth paas.Auth) (map[string]Pool, error) { func GetUserPools(auth paas.Auth) (map[string]paas.Pool, error) {
pools := map[string]Pool{} pools := map[string]paas.Pool{}
// get all pools // get all pools
body := map[string]any{} body := map[string]any{}
@@ -112,10 +232,6 @@ func GetUserPools(auth paas.Auth) (map[string]Pool, error) {
// for each pool // for each pool
for poolname, pool := range pools { for poolname, pool := range pools {
// for each resource in pool data // for each resource in pool data
// create pool charts map
pool.ResourceCharts = make(map[string]map[string]any)
for k, v := range pool.Resources { for k, v := range pool.Resources {
m := meta[k].(map[string]any) m := meta[k].(map[string]any)
t := m["type"].(string) t := m["type"].(string)
@@ -123,122 +239,49 @@ func GetUserPools(auth paas.Auth) (map[string]Pool, error) {
category := m["category"].(string) category := m["category"].(string)
// create a category if it does not already exist // create a category if it does not already exist
if _, ok := pool.ResourceCharts[category]; !ok { if _, ok := pool.Resources[category]; !ok {
pool.ResourceCharts[category] = map[string]any{} pool.Resources[category] = map[string]any{}
} }
// depending on type, decode the apool data into the corresponding resource type // depending on type, decode the pool data into the corresponding resource type
switch t { switch t {
case "numeric": case "numeric":
n := paas.NumericResource{} n := NumericResource{}
n.Type = t n.Type = t
err_m := mapstructure.Decode(m, &n) err_m := mapstructure.Decode(m, &n)
err_r := mapstructure.Decode(r, &n) err_r := mapstructure.Decode(r, &n)
if err_m != nil || err_r != nil { if err_m != nil || err_r != nil {
return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error()) return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
} }
pool.ResourceCharts[category][k] = n pools[poolname].Resources[category].(map[string]any)[k] = n
case "storage": case "storage":
n := paas.StorageResource{} n := StorageResource{}
n.Type = t n.Type = t
err_m := mapstructure.Decode(m, &n) err_m := mapstructure.Decode(m, &n)
err_r := mapstructure.Decode(r, &n) err_r := mapstructure.Decode(r, &n)
if err_m != nil || err_r != nil { if err_m != nil || err_r != nil {
return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error()) return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
} }
pool.ResourceCharts[category][k] = n pools[poolname].Resources[category].(map[string]any)[k] = n
case "list": case "list":
n := paas.ListResource{} n := ListResource{}
n.Type = t n.Type = t
err_m := mapstructure.Decode(m, &n) err_m := mapstructure.Decode(m, &n)
err_r := mapstructure.Decode(r, &n) err_r := mapstructure.Decode(r, &n)
if err_m != nil || err_r != nil { if err_m != nil || err_r != nil {
return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error()) return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
} }
pool.ResourceCharts[category][k] = n pools[poolname].Resources[category].(map[string]any)[k] = n
} }
// delete the old entry, only categories should be left at the end of the loop // delete the old entry, only categories should be left at the end of the loop
//delete(pools[poolname].Resources, k) delete(pools[poolname].Resources, k)
} }
pools[poolname] = pool
}
err = FormatPoolResourceCharts(&pools)
if err != nil {
return pools, err
} }
return pools, nil return pools, nil
} }
func FormatPoolResourceCharts(pools *map[string]Pool) error {
for poolname, pool := range *pools {
// for each resource category
for categoryname, category := range pool.ResourceCharts {
// for each resource in each category
for resourcename, resource := range category {
// create a resource chart for resource depending on resource type
switch t := resource.(type) {
case paas.NumericResource:
avail, prefix := paas.FormatNumber(paas.SafeUint64(t.Total.Avail*t.Multiplier), t.Base)
(*pools)[poolname].ResourceCharts[categoryname][resourcename] = 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 paas.StorageResource:
avail, prefix := paas.FormatNumber(paas.SafeUint64(t.Total.Avail*t.Multiplier), t.Base)
(*pools)[poolname].ResourceCharts[categoryname][resourcename] = 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 paas.ListResource:
l := struct {
Type string
Display bool
Resources []ResourceChart
}{
Type: t.Type,
Display: t.Display,
Resources: []ResourceChart{},
}
for _, r := range t.Total {
avail := fmt.Sprintf("%d", r.Avail)
l.Resources = append(l.Resources, ResourceChart{
Type: t.Type,
Display: t.Display,
Name: r.Name,
Used: r.Used,
Max: r.Max,
Avail: avail, // usually an int
Unit: "",
ColorHex: InterpolateColorHSV(Green, Red, float64(r.Used)/float64(r.Max)).ToHTML(),
})
}
(*pools)[poolname].ResourceCharts[categoryname][resourcename] = l
}
}
}
}
return nil
}
// interpolate between min and max by normalized (0 - 1) val // interpolate between min and max by normalized (0 - 1) val
func InterpolateColorHSV(min color.RGB, max color.RGB, val float64) color.RGB { func InterpolateColorHSV(min color.RGB, max color.RGB, val float64) color.RGB {
minhsl := min.ToHSL() minhsl := min.ToHSL()
+7 -5
View File
@@ -12,6 +12,8 @@ import (
"github.com/go-viper/mapstructure/v2" "github.com/go-viper/mapstructure/v2"
) )
// imported types from fabric
type InstanceConfig struct { type InstanceConfig struct {
paas.Instance `mapstructure:",squash"` paas.Instance `mapstructure:",squash"`
// overrides // overrides
@@ -26,8 +28,8 @@ type GlobalConfig struct {
type PoolConfig struct { type PoolConfig struct {
CPU struct { CPU struct {
Global []paas.Match Global []paas.MatchLimit
Nodes map[string][]paas.Match Nodes map[string][]paas.MatchLimit
} }
} }
@@ -241,7 +243,7 @@ func GetCPUTypes(vm paas.InstancePath, pool string, auth paas.Auth) (common.Sele
} }
// use node specific rules if present, otherwise use global rules // use node specific rules if present, otherwise use global rules
var userCPU []paas.Match var userCPU []paas.MatchLimit
if _, ok := poolCPUConfig.CPU.Nodes[vm.NodeName]; ok { if _, ok := poolCPUConfig.CPU.Nodes[vm.NodeName]; ok {
userCPU = poolCPUConfig.CPU.Nodes[vm.NodeName] userCPU = poolCPUConfig.CPU.Nodes[vm.NodeName]
} else { } else {
@@ -267,7 +269,7 @@ func GetCPUTypes(vm paas.InstancePath, pool string, auth paas.Auth) (common.Sele
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res) return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
} }
supported := struct { supported := struct {
data []paas.Match data []paas.MatchLimit
}{} }{}
err = mapstructure.Decode(body, supported) err = mapstructure.Decode(body, supported)
if err != nil { if err != nil {
@@ -276,7 +278,7 @@ func GetCPUTypes(vm paas.InstancePath, pool string, auth paas.Auth) (common.Sele
// 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 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 { for _, cpu := range supported.data {
contains := slices.ContainsFunc(userCPU, func(c paas.Match) bool { contains := slices.ContainsFunc(userCPU, func(c paas.MatchLimit) bool {
return c.Name == cpu.Name return c.Name == cpu.Name
}) })
if !contains { if !contains {
+5
View File
@@ -9,6 +9,11 @@ import (
"github.com/go-viper/mapstructure/v2" "github.com/go-viper/mapstructure/v2"
) )
// used when requesting GET /access/domains
type GetRealmsBody struct {
Data []Realm `json:"data"`
}
// stores each realm's data // stores each realm's data
type Realm struct { type Realm struct {
Default int `json:"default"` Default int `json:"default"`
-3
View File
@@ -1,3 +0,0 @@
web/modules/*
WFA-JS/*
dist/*
+3 -7
View File
@@ -1,11 +1,8 @@
import { defineConfig, globalIgnores } from "eslint/config"; import { defineConfig } from "eslint/config";
import globals from "globals"; import globals from "globals";
import js from "@eslint/js"; import js from "@eslint/js";
export default defineConfig([ export default defineConfig([js.configs.recommended,{
js.configs.recommended,
globalIgnores(["dist/", "web/modules", "WFA-JS"]),
{
languageOptions: { languageOptions: {
globals: { globals: {
...globals.browser, ...globals.browser,
@@ -29,5 +26,4 @@ export default defineConfig([
}], }],
"prefer-const": ["error"] "prefer-const": ["error"]
}, },
} }]);
]);
+18 -21
View File
@@ -1,26 +1,23 @@
module proxmoxaas-dashboard module proxmoxaas-dashboard
go 1.27.0 go 1.26.5
require ( require (
github.com/gerow/go-color v0.0.0-20140219113758-125d37f527f1 github.com/gerow/go-color v0.0.0-20140219113758-125d37f527f1
github.com/gin-gonic/gin v1.12.0 github.com/gin-gonic/gin v1.12.0
github.com/go-viper/mapstructure/v2 v2.5.0 github.com/go-viper/mapstructure/v2 v2.5.0
github.com/tdewolff/minify/v2 v2.24.17 github.com/tdewolff/minify/v2 v2.24.13
proxmoxaas-common-lib v0.0.0 proxmoxaas-common-lib v0.0.0
web v0.0.0
) )
replace proxmoxaas-common-lib => ./proxmoxaas-common-lib replace proxmoxaas-common-lib => ./proxmoxaas-common-lib
replace web => ./dist/web
require ( require (
github.com/bytedance/gopkg v0.1.4 // indirect github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.2 // indirect github.com/bytedance/sonic v1.15.2 // indirect
github.com/bytedance/sonic/loader v0.5.2 // indirect github.com/bytedance/sonic/loader v0.5.1 // indirect
github.com/cloudwego/base64x v0.1.7 // indirect github.com/cloudwego/base64x v0.1.7 // indirect
github.com/gabriel-vasile/mimetype v1.4.15 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.1 // indirect github.com/gin-contrib/sse v1.1.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
@@ -28,22 +25,22 @@ require (
github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.5.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-isatty v0.0.22 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect github.com/pelletier/go-toml/v2 v2.4.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.61.0 // indirect github.com/quic-go/quic-go v0.60.0 // indirect
github.com/tdewolff/parse/v2 v2.8.16 // indirect github.com/tdewolff/parse/v2 v2.8.13 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.2 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.8.1 // indirect go.mongodb.org/mongo-driver/v2 v2.6.1 // indirect
golang.org/x/arch v0.30.0 // indirect golang.org/x/arch v0.28.0 // indirect
golang.org/x/crypto v0.55.0 // indirect golang.org/x/crypto v0.53.0 // indirect
golang.org/x/net v0.58.0 // indirect golang.org/x/net v0.56.0 // indirect
golang.org/x/sys v0.47.0 // indirect golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.41.0 // indirect golang.org/x/text v0.38.0 // indirect
google.golang.org/protobuf v1.36.12 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )
+18
View File
@@ -0,0 +1,18 @@
{
"name": "proxmoxaas-dashboard",
"version": "1.0.0",
"description": "Front-end for ProxmoxAAS",
"type": "module",
"scripts": {
"lint": "html-validate --config dev_config/.htmlvalidate.json web/html/*; stylelint --config dev_config/.stylelintrc.json --formatter verbose --fix web/css/*.css; DEBUG=eslint:cli-engine eslint --config dev_config/eslint.config.mjs --fix web/scripts/"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.5",
"@eslint/js": "^10.0.1",
"eslint": "^10.2.1",
"globals": "^17.5.0",
"html-validate": "^9.4.0",
"stylelint": "^15.9.0",
"stylelint-config-standard": "^33.0.0"
}
}
+1 -14
View File
@@ -130,19 +130,6 @@ hr {
align-items: center; align-items: center;
} }
.row-reverse {
flex-direction: row-reverse;
row-gap: 10px;
align-items: center;
}
.column {
flex-direction: column;
row-gap: 10px;
align-items: center;
}
.column-reverse { .column-reverse {
flex-direction: column-reverse; flex-direction: column-reverse;
row-gap: 10px; row-gap: 10px;
@@ -202,7 +189,7 @@ hr {
} }
/* fixes edge case in w3-hide-small when width between 600 and 601 */ /* fixes edge case in w3-hide-small when width between 600 and 601 */
@media screen and (width <= 601px) and (width >= 440px) { @media screen and (width <=601px) {
.hide-large {display: none !important;} .hide-large {display: none !important;}
.hide-medium {display:none !important} .hide-medium {display:none !important}
.hide-small {display:none !important} .hide-small {display:none !important}
-3
View File
@@ -1,3 +0,0 @@
module web
go 1.26.0
+5 -5
View File
@@ -39,11 +39,11 @@
<h2>Account</h2> <h2>Account</h2>
<section class="w3-card w3-padding"> <section class="w3-card w3-padding">
<h3>Account Details</h3> <h3>Account Details</h3>
<p id="username">Username: {{.user.Username.UserID}}@{{.user.Username.Realm}}</p> <p id="username">Username: {{.account.Username.UserID}}@{{.account.Username.Realm}}</p>
<p id="email">Email: {{.user.Mail}}</p> <p id="email">Email: {{.account.Mail}}</p>
<p>Password: <button class="w3-button" id="change-password" type="button" style="padding: 0em; height: 1.5em; line-height: 1.5em;">Change Password</button></p> <p>Password: <button class="w3-button" id="change-password" type="button" style="padding: 0em; height: 1.5em; line-height: 1.5em;">Change Password</button></p>
</section> </section>
{{range $poolname, $pool := .pools}} {{range $poolname, $pool := .account.Pools}}
{{template "pool-resources" $pool}} {{template "pool-resources" $pool}}
{{end}} {{end}}
</main> </main>
@@ -58,9 +58,9 @@
<div id="body"> <div id="body">
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form"> <form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="new-password">New Password</label> <label for="new-password">New Password</label>
<input class="w3-input w3-border" id="new-password" name="new-password" type="password" autocomplete="new-password" required> <input class="w3-input w3-border" id="new-password" name="new-password" type="password" required>
<label for="confirm-password">Confirm Password</label> <label for="confirm-password">Confirm Password</label>
<input class="w3-input w3-border" id="confirm-password" name="confirm-password" type="password" autocomplete="new-password" required> <input class="w3-input w3-border" id="confirm-password" name="confirm-password" type="password" required>
</form> </form>
</div> </div>
<div id="controls" class="w3-center w3-container"> <div id="controls" class="w3-center w3-container">
+2 -2
View File
@@ -114,9 +114,9 @@
<label class="container-specific none" for="rootfs-size">ROOTFS Size (GiB)</label> <label class="container-specific none" for="rootfs-size">ROOTFS Size (GiB)</label>
<input class="w3-input w3-border container-specific none" name="rootfs-size" id="rootfs-size" type="number" min="0" max="131072" required disabled> <input class="w3-input w3-border container-specific none" name="rootfs-size" id="rootfs-size" type="number" min="0" max="131072" required disabled>
<label class="container-specific none" for="password">Password</label> <label class="container-specific none" for="password">Password</label>
<input class="w3-input w3-border container-specific none" name="password" id="password" type="password" autocomplete="new-password" required disabled> <input class="w3-input w3-border container-specific none" name="password" id="password" type="password" required disabled>
<label class="container-specific none" for="confirm-password">Confirm Password</label> <label class="container-specific none" for="confirm-password">Confirm Password</label>
<input class="w3-input w3-border container-specific none" name="confirm-password" id="confirm-password" type="password" autocomplete="new-password" required disabled> <input class="w3-input w3-border container-specific none" name="confirm-password" id="confirm-password" type="password" required disabled>
</form> </form>
</div> </div>
<div id="controls" class="w3-center w3-container"> <div id="controls" class="w3-center w3-container">
+297 -322
View File
@@ -1,26 +1,53 @@
// Copyright 2018 The Go Authors. All rights reserved. // Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//
"use strict"; // This file has been modified for use by the TinyGo compiler.
(() => { (() => {
// Map multiple JavaScript environments to a single common API,
// preferring web standards over Node.js API.
//
// Environments considered:
// - Browsers
// - Node.js
// - Electron
// - Parcel
if (typeof global !== "undefined") {
// global already exists
} else if (typeof window !== "undefined") {
window.global = window;
} else if (typeof self !== "undefined") {
self.global = self;
} else {
throw new Error("cannot export Go (neither global, window nor self is defined)");
}
if (!global.require && typeof require !== "undefined") {
global.require = require;
}
if (!global.fs && global.require) {
global.fs = require("node:fs");
}
const enosys = () => { const enosys = () => {
const err = new Error("not implemented"); const err = new Error("not implemented");
err.code = "ENOSYS"; err.code = "ENOSYS";
return err; return err;
}; };
if (!globalThis.fs) { if (!global.fs) {
let outputBuf = ""; let outputBuf = "";
globalThis.fs = { global.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) { writeSync(fd, buf) {
outputBuf += decoder.decode(buf); outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n"); const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) { if (nl != -1) {
console.log(outputBuf.substring(0, nl)); console.log(outputBuf.substr(0, nl));
outputBuf = outputBuf.substring(nl + 1); outputBuf = outputBuf.substr(nl + 1);
} }
return buf.length; return buf.length;
}, },
@@ -58,8 +85,8 @@
}; };
} }
if (!globalThis.process) { if (!global.process) {
globalThis.process = { global.process = {
getuid() { return -1; }, getuid() { return -1; },
getgid() { return -1; }, getgid() { return -1; },
geteuid() { return -1; }, geteuid() { return -1; },
@@ -73,66 +100,53 @@
} }
} }
if (!globalThis.path) { if (!global.crypto) {
globalThis.path = { const nodeCrypto = require("node:crypto");
resolve(...pathSegments) { global.crypto = {
return pathSegments.join("/"); getRandomValues(b) {
} nodeCrypto.randomFillSync(b);
} },
};
} }
if (!globalThis.crypto) { if (!global.performance) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); global.performance = {
now() {
const [sec, nsec] = process.hrtime();
return sec * 1000 + nsec / 1000000;
},
};
} }
if (!globalThis.performance) { if (!global.TextEncoder) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); global.TextEncoder = require("node:util").TextEncoder;
} }
if (!globalThis.TextEncoder) { if (!global.TextDecoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required"); global.TextDecoder = require("node:util").TextDecoder;
} }
if (!globalThis.TextDecoder) { // End of polyfills for common API.
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder("utf-8"); const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8"); const decoder = new TextDecoder("utf-8");
let reinterpretBuf = new DataView(new ArrayBuffer(8));
var logLine = [];
const wasmExit = {}; // thrown to exit via proc_exit (not an error)
globalThis.Go = class { global.Go = class {
constructor() { constructor() {
this.argv = ["js"]; this._callbackTimeouts = new Map();
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1; this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => { const mem = () => {
this.mem.setUint32(addr + 0, v, true); // The buffer may change when requesting more memory.
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); return new DataView(this._inst.exports.memory.buffer);
} }
const setInt32 = (addr, v) => { const unboxValue = (v_ref) => {
this.mem.setUint32(addr + 0, v, true); reinterpretBuf.setBigInt64(0, v_ref, true);
} const f = reinterpretBuf.getFloat64(0, true);
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) { if (f === 0) {
return undefined; return undefined;
} }
@@ -140,69 +154,77 @@
return f; return f;
} }
const id = this.mem.getUint32(addr, true); const id = v_ref & 0xffffffffn;
return this._values[id]; return this._values[id];
} }
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) { const loadValue = (addr) => {
let v_ref = mem().getBigUint64(addr, true);
return unboxValue(v_ref);
}
const boxValue = (v) => {
const nanHead = 0x7FF80000n;
if (typeof v === "number") {
if (isNaN(v)) { if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true); return nanHead << 32n;
this.mem.setUint32(addr, 0, true);
return;
} }
this.mem.setFloat64(addr, v, true); if (v === 0) {
return; return (nanHead << 32n) | 1n;
}
reinterpretBuf.setFloat64(0, v, true);
return reinterpretBuf.getBigInt64(0, true);
} }
if (v === undefined) { switch (v) {
this.mem.setFloat64(addr, 0, true); case undefined:
return; return 0n;
case null:
return (nanHead << 32n) | 2n;
case true:
return (nanHead << 32n) | 3n;
case false:
return (nanHead << 32n) | 4n;
} }
let id = this._ids.get(v); let id = this._ids.get(v);
if (id === undefined) { if (id === undefined) {
id = this._idPool.pop(); id = this._idPool.pop();
if (id === undefined) { if (id === undefined) {
id = this._values.length; id = BigInt(this._values.length);
} }
this._values[id] = v; this._values[id] = v;
this._goRefCounts[id] = 0; this._goRefCounts[id] = 0;
this._ids.set(v, id); this._ids.set(v, id);
} }
this._goRefCounts[id]++; this._goRefCounts[id]++;
let typeFlag = 0; let typeFlag = 1n;
switch (typeof v) { switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string": case "string":
typeFlag = 2; typeFlag = 2n;
break; break;
case "symbol": case "symbol":
typeFlag = 3; typeFlag = 3n;
break; break;
case "function": case "function":
typeFlag = 4; typeFlag = 4n;
break; break;
} }
this.mem.setUint32(addr + 4, nanHead | typeFlag, true); return id | ((nanHead | typeFlag) << 32n);
this.mem.setUint32(addr, id, true);
} }
const loadSlice = (addr) => { const storeValue = (addr, v) => {
const array = getInt64(addr + 0); let v_ref = boxValue(v);
const len = getInt64(addr + 8); mem().setBigUint64(addr, v_ref, true);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
} }
const loadSliceOfValues = (addr) => { const loadSlice = (array, len, cap) => {
const array = getInt64(addr + 0); return new Uint8Array(this._inst.exports.memory.buffer, array, len);
const len = getInt64(addr + 8); }
const loadSliceOfValues = (array, len, cap) => {
const a = new Array(len); const a = new Array(len);
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8); a[i] = loadValue(array + i * 8);
@@ -210,109 +232,81 @@
return a; return a;
} }
const loadString = (addr) => { const loadString = (ptr, len) => {
const saddr = getInt64(addr + 0); return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len));
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const testCallExport = (a, b) => {
this._inst.exports.testExport0();
return this._inst.exports.testExport(a, b);
} }
const timeOrigin = Date.now() - performance.now(); const timeOrigin = Date.now() - performance.now();
this.importObject = { this.importObject = {
_gotest: { wasi_snapshot_preview1: {
add: (a, b) => a + b, // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write
callExport: testCallExport, fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) {
let nwritten = 0;
if (fd == 1) {
for (let iovs_i=0; iovs_i<iovs_len;iovs_i++) {
let iov_ptr = iovs_ptr+iovs_i*8; // assuming wasm32
let ptr = mem().getUint32(iov_ptr + 0, true);
let len = mem().getUint32(iov_ptr + 4, true);
nwritten += len;
for (let i=0; i<len; i++) {
let c = mem().getUint8(ptr+i);
if (c == 13) { // CR
// ignore
} else if (c == 10) { // LF
// write line
let line = decoder.decode(new Uint8Array(logLine));
logLine = [];
console.log(line);
} else {
logLine.push(c);
}
}
}
} else {
console.error('invalid file descriptor:', fd);
}
mem().setUint32(nwritten_ptr, nwritten, true);
return 0;
},
fd_close: () => 0, // dummy
fd_fdstat_get: () => 0, // dummy
fd_seek: () => 0, // dummy
proc_exit: (code) => {
this.exited = true;
this.exitCode = code;
this._resolveExitPromise();
throw wasmExit;
},
random_get: (bufPtr, bufLen) => {
crypto.getRandomValues(loadSlice(bufPtr, bufLen));
return 0;
},
}, },
gojs: { gojs: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) // func ticks() int64
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported "runtime.ticks": () => {
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). return BigInt((timeOrigin + performance.now()) * 1e6);
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
}, },
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) // func sleepTicks(timeout int64)
"runtime.wasmWrite": (sp) => { "runtime.sleepTicks": (timeout) => {
sp >>>= 0; // Do not sleep, only reactivate scheduler after the given timeout.
const fd = getInt64(sp + 8); setTimeout(() => {
const p = getInt64(sp + 16); if (this.exited) return;
const n = this.mem.getInt32(sp + 24, true); try {
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); this._inst.exports.go_scheduler();
}, } catch (e) {
if (e !== wasmExit) throw e;
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
} }
}, }, Number(timeout)/1e6);
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
}, },
// func finalizeRef(v ref) // func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => { "syscall/js.finalizeRef": (v_ref) => {
sp >>>= 0; // Note: TinyGo does not support finalizers so this is only called
const id = this.mem.getUint32(sp + 8, true); // for one specific case, by js.go:jsString. and can/might leak memory.
const id = v_ref & 0xffffffffn;
if (this._goRefCounts?.[id] !== undefined) {
this._goRefCounts[id]--; this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) { if (this._goRefCounts[id] === 0) {
const v = this._values[id]; const v = this._values[id];
@@ -320,243 +314,205 @@
this._ids.delete(v); this._ids.delete(v);
this._idPool.push(id); this._idPool.push(id);
} }
} else {
console.error("syscall/js.finalizeRef: unknown id", id);
}
}, },
// func stringVal(value string) ref // func stringVal(value string) ref
"syscall/js.stringVal": (sp) => { "syscall/js.stringVal": (value_ptr, value_len) => {
sp >>>= 0; value_ptr >>>= 0;
storeValue(sp + 24, loadString(sp + 8)); const s = loadString(value_ptr, value_len);
return boxValue(s);
}, },
// func valueGet(v ref, p string) ref // func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => { "syscall/js.valueGet": (v_ref, p_ptr, p_len) => {
sp >>>= 0; let prop = loadString(p_ptr, p_len);
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); let v = unboxValue(v_ref);
sp = this._inst.exports.getsp() >>> 0; // see comment above let result = Reflect.get(v, prop);
storeValue(sp + 32, result); return boxValue(result);
}, },
// func valueSet(v ref, p string, x ref) // func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => { "syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => {
sp >>>= 0; const v = unboxValue(v_ref);
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); const p = loadString(p_ptr, p_len);
const x = unboxValue(x_ref);
Reflect.set(v, p, x);
}, },
// func valueDelete(v ref, p string) // func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => { "syscall/js.valueDelete": (v_ref, p_ptr, p_len) => {
sp >>>= 0; const v = unboxValue(v_ref);
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); const p = loadString(p_ptr, p_len);
Reflect.deleteProperty(v, p);
}, },
// func valueIndex(v ref, i int) ref // func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => { "syscall/js.valueIndex": (v_ref, i) => {
sp >>>= 0; return boxValue(Reflect.get(unboxValue(v_ref), i));
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
}, },
// valueSetIndex(v ref, i int, x ref) // valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => { "syscall/js.valueSetIndex": (v_ref, i, x_ref) => {
sp >>>= 0; Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref));
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
}, },
// func valueCall(v ref, m string, args []ref) (ref, bool) // func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => { "syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => {
sp >>>= 0; const v = unboxValue(v_ref);
const name = loadString(m_ptr, m_len);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
try { try {
const v = loadValue(sp + 8); const m = Reflect.get(v, name);
const m = Reflect.get(v, loadString(sp + 16)); storeValue(ret_addr, Reflect.apply(m, v, args));
const args = loadSliceOfValues(sp + 32); mem().setUint8(ret_addr + 8, 1);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) { } catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above storeValue(ret_addr, err);
storeValue(sp + 56, err); mem().setUint8(ret_addr + 8, 0);
this.mem.setUint8(sp + 64, 0);
} }
}, },
// func valueInvoke(v ref, args []ref) (ref, bool) // func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => { "syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
sp >>>= 0;
try { try {
const v = loadValue(sp + 8); const v = unboxValue(v_ref);
const args = loadSliceOfValues(sp + 16); const args = loadSliceOfValues(args_ptr, args_len, args_cap);
const result = Reflect.apply(v, undefined, args); storeValue(ret_addr, Reflect.apply(v, undefined, args));
sp = this._inst.exports.getsp() >>> 0; // see comment above mem().setUint8(ret_addr + 8, 1);
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) { } catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above storeValue(ret_addr, err);
storeValue(sp + 40, err); mem().setUint8(ret_addr + 8, 0);
this.mem.setUint8(sp + 48, 0);
} }
}, },
// func valueNew(v ref, args []ref) (ref, bool) // func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => { "syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
sp >>>= 0; const v = unboxValue(v_ref);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
try { try {
const v = loadValue(sp + 8); storeValue(ret_addr, Reflect.construct(v, args));
const args = loadSliceOfValues(sp + 16); mem().setUint8(ret_addr + 8, 1);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) { } catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above storeValue(ret_addr, err);
storeValue(sp + 40, err); mem().setUint8(ret_addr+ 8, 0);
this.mem.setUint8(sp + 48, 0);
} }
}, },
// func valueLength(v ref) int // func valueLength(v ref) int
"syscall/js.valueLength": (sp) => { "syscall/js.valueLength": (v_ref) => {
sp >>>= 0; return unboxValue(v_ref).length;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
}, },
// valuePrepareString(v ref) (ref, int) // valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => { "syscall/js.valuePrepareString": (ret_addr, v_ref) => {
sp >>>= 0; const s = String(unboxValue(v_ref));
const str = encoder.encode(String(loadValue(sp + 8))); const str = encoder.encode(s);
storeValue(sp + 16, str); storeValue(ret_addr, str);
setInt64(sp + 24, str.length); mem().setInt32(ret_addr + 8, str.length, true);
}, },
// valueLoadString(v ref, b []byte) // valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => { "syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => {
sp >>>= 0; const str = unboxValue(v_ref);
const str = loadValue(sp + 8); loadSlice(slice_ptr, slice_len, slice_cap).set(str);
loadSlice(sp + 16).set(str);
}, },
// func valueInstanceOf(v ref, t ref) bool // func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => { "syscall/js.valueInstanceOf": (v_ref, t_ref) => {
sp >>>= 0; return unboxValue(v_ref) instanceof unboxValue(t_ref);
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
}, },
// func copyBytesToGo(dst []byte, src ref) (int, bool) // func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => { "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => {
sp >>>= 0; let num_bytes_copied_addr = ret_addr;
const dst = loadSlice(sp + 8); let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
const src = loadValue(sp + 32);
const dst = loadSlice(dest_addr, dest_len);
const src = unboxValue(src_ref);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0); mem().setUint8(returned_status_addr, 0); // Return "not ok" status
return; return;
} }
const toCopy = src.subarray(0, dst.length); const toCopy = src.subarray(0, dst.length);
dst.set(toCopy); dst.set(toCopy);
setInt64(sp + 40, toCopy.length); mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
this.mem.setUint8(sp + 48, 1); mem().setUint8(returned_status_addr, 1); // Return "ok" status
}, },
// func copyBytesToJS(dst ref, src []byte) (int, bool) // copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => { // Originally copied from upstream Go project, then modified:
sp >>>= 0; // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416
const dst = loadValue(sp + 8); "syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => {
const src = loadSlice(sp + 16); let num_bytes_copied_addr = ret_addr;
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
const dst = unboxValue(dst_ref);
const src = loadSlice(src_addr, src_len);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0); mem().setUint8(returned_status_addr, 0); // Return "not ok" status
return; return;
} }
const toCopy = src.subarray(0, dst.length); const toCopy = src.subarray(0, dst.length);
dst.set(toCopy); dst.set(toCopy);
setInt64(sp + 40, toCopy.length); mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
this.mem.setUint8(sp + 48, 1); mem().setUint8(returned_status_addr, 1); // Return "ok" status
},
"debug": (value) => {
console.log(value);
}, },
} }
}; };
// Go 1.20 uses 'env'. Go 1.21 uses 'gojs'.
// For compatibility, we use both as long as Go 1.20 is supported.
this.importObject.env = this.importObject.gojs;
} }
async run(instance) { async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance; this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN, NaN,
0, 0,
null, null,
true, true,
false, false,
globalThis, global,
this, this,
]; ];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids this._ids = new Map(); // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited this.exited = false; // whether the Go program has exited
this.exitCode = 0;
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. if (this._inst.exports._start) {
let offset = 4096; let exitPromise = new Promise((resolve, reject) => {
this._resolveExitPromise = resolve;
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
}); });
// The linker guarantees global data starts from at least wasmMinDataAddr. // Run program, but catch the wasmExit exception that's thrown
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. // to return back here.
const wasmMinDataAddr = 4096 + 8192; try {
if (offset >= wasmMinDataAddr) { this._inst.exports._start();
throw new Error("total length of command line and environment variables exceeds limit"); } catch (e) {
if (e !== wasmExit) throw e;
} }
this._inst.exports.run(argc, argv); await exitPromise;
if (this.exited) { return this.exitCode;
this._resolveExitPromise(); } else {
this._inst.exports._initialize();
} }
await this._exitPromise;
} }
_resume() { _resume() {
if (this.exited) { if (this.exited) {
throw new Error("Go program has already exited"); throw new Error("Go program has already exited");
} }
try {
this._inst.exports.resume(); this._inst.exports.resume();
} catch (e) {
if (e !== wasmExit) throw e;
}
if (this.exited) { if (this.exited) {
this._resolveExitPromise(); this._resolveExitPromise();
} }
@@ -572,20 +528,39 @@
}; };
} }
} }
if (
global.require &&
global.require.main === module &&
global.process &&
global.process.versions &&
!global.process.versions.electron
) {
if (process.argv.length != 3) {
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
process.exit(1);
}
const go = new Go();
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then(async (result) => {
let exitCode = await go.run(result.instance);
process.exit(exitCode);
}).catch((err) => {
console.error(err);
process.exit(1);
});
}
})(); })();
// wasm setup code // wasm setup code
export default function init (path) { export default function init (path) {
if (typeof global === "undefined") {
var global = globalThis;
}
return new Promise ((res) => { return new Promise ((res) => {
const go = new Go(); const go = new Go();
const run = (obj) => { const run = (obj) => {
const wasm = obj.instance; const wasm = obj.instance;
global.wfa = wasm global.wfa = wasm
go.run(wasm); go.run(wasm);
res(wasm) res()
} }
if ('instantiateStreaming' in WebAssembly) { if ('instantiateStreaming' in WebAssembly) {
WebAssembly.instantiateStreaming(fetch(path), go.importObject).then(function (obj) { WebAssembly.instantiateStreaming(fetch(path), go.importObject).then(function (obj) {
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -1,5 +1,5 @@
import { requestAPI, setAppearance } from "./utils.js"; import { requestAPI, setAppearance } from "./utils.js";
import { dialog, error } from "./dialog.js"; import { dialog } from "./dialog.js";
window.addEventListener("DOMContentLoaded", init); window.addEventListener("DOMContentLoaded", init);
@@ -15,7 +15,7 @@ function handlePasswordChangeButton () {
if (result === "confirm") { if (result === "confirm") {
const result = await requestAPI("/access/password", "POST", { password: form.get("new-password") }); const result = await requestAPI("/access/password", "POST", { password: form.get("new-password") });
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to change password but got: ${result.error}`); alert(`Attempted to change password but got: ${result.error}`);
} }
} }
}); });
+11 -7
View File
@@ -1,5 +1,5 @@
import { requestAPI, getURIData, setAppearance, requestDash } from "./utils.js"; import { requestAPI, getURIData, setAppearance, requestDash } from "./utils.js";
import { error, dialog } from "./dialog.js"; import { alert, dialog } from "./dialog.js";
window.addEventListener("DOMContentLoaded", init); window.addEventListener("DOMContentLoaded", init);
@@ -74,7 +74,7 @@ class BackupCard extends HTMLElement {
}; };
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/backup/notes`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/backup/notes`, "POST", body);
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to edit backup but got: ${result.error}`); alert(`Attempted to edit backup but got: ${result.error}`);
} }
refreshBackups(); refreshBackups();
} }
@@ -90,7 +90,7 @@ class BackupCard extends HTMLElement {
}; };
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/backup`, "DELETE", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/backup`, "DELETE", body);
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to delete backup but got: ${result.error}`); alert(`Attempted to delete backup but got: ${result.error}`);
} }
refreshBackups(); refreshBackups();
} }
@@ -106,7 +106,7 @@ class BackupCard extends HTMLElement {
}; };
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/backup/restore`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/backup/restore`, "POST", body);
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to delete backup but got: ${result.error}`); alert(`Attempted to delete backup but got: ${result.error}`);
} }
refreshBackups(); refreshBackups();
} }
@@ -116,10 +116,14 @@ class BackupCard extends HTMLElement {
customElements.define("backup-card", BackupCard); customElements.define("backup-card", BackupCard);
async function getBackupsFragment () {
return await requestDash(`/backups/backups?node=${node}&type=${type}&vmid=${vmid}`, "GET");
}
async function refreshBackups () { async function refreshBackups () {
let backups = await requestDash(`/backups/backups?node=${node}&type=${type}&vmid=${vmid}`, "GET"); let backups = await getBackupsFragment();
if (backups.status !== 200) { if (backups.status !== 200) {
error("Error fetching backups."); alert("Error fetching backups.");
} }
else { else {
backups = backups.data; backups = backups.data;
@@ -137,7 +141,7 @@ async function handleBackupAddButton () {
}; };
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/backup`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/backup`, "POST", body);
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to create backup but got: ${result.error}`); alert(`Attempted to create backup but got: ${result.error}`);
} }
refreshBackups(); refreshBackups();
} }
+2 -3
View File
@@ -1,5 +1,4 @@
import { getSetting, requestAPI } from "./utils.js"; import { getSetting, requestAPI } from "./utils.js";
import { error } from "./dialog.js";
export async function setupClientSync (callback) { export async function setupClientSync (callback) {
const scheme = getSetting("sync-scheme"); const scheme = getSetting("sync-scheme");
@@ -32,12 +31,12 @@ export async function setupClientSync (callback) {
callback(); callback();
} }
else { else {
error("clientsync: recieved unexpected message from server, closing socket."); console.error("clientsync: recieved unexpected message from server, closing socket.");
socket.close(); socket.close();
} }
}); });
} }
else { else {
error(`clientsync: unsupported scheme ${scheme} selected.`); console.error(`clientsync: unsupported scheme ${scheme} selected.`);
} }
} }
+23 -23
View File
@@ -1,5 +1,5 @@
import { requestPVE, requestAPI, goToPage, getURIData, setAppearance, setIconSrc, requestDash, getCookie } from "./utils.js"; import { requestPVE, requestAPI, goToPage, getURIData, setAppearance, setIconSrc, requestDash } from "./utils.js";
import { error, dialog } from "./dialog.js"; import { alert, dialog } from "./dialog.js";
window.addEventListener("DOMContentLoaded", init); window.addEventListener("DOMContentLoaded", init);
@@ -59,7 +59,7 @@ class VolumeAction extends HTMLElement {
this.setStatusLoading(); this.setStatusLoading();
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/detach`, "POST"); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/detach`, "POST");
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to detach ${disk} but got: ${result.error}`); alert(`Attempted to detach ${disk} but got: ${result.error}`);
} }
refreshVolumes(); refreshVolumes();
refreshBoot(); refreshBoot();
@@ -80,7 +80,7 @@ class VolumeAction extends HTMLElement {
const disk = `${prefix}${device}`; const disk = `${prefix}${device}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/attach`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/attach`, "POST", body);
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to attach ${this.dataset.volume} to ${disk} but got: ${result.error}`); alert(`Attempted to attach ${this.dataset.volume} to ${disk} but got: ${result.error}`);
} }
refreshVolumes(); refreshVolumes();
refreshBoot(); refreshBoot();
@@ -98,7 +98,7 @@ class VolumeAction extends HTMLElement {
}; };
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/resize`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/resize`, "POST", body);
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to resize ${disk} but got: ${result.error}`); alert(`Attempted to resize ${disk} but got: ${result.error}`);
} }
refreshVolumes(); refreshVolumes();
refreshBoot(); refreshBoot();
@@ -117,7 +117,7 @@ class VolumeAction extends HTMLElement {
}; };
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/move`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/move`, "POST", body);
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to move ${disk} to ${body.storage} but got: ${result.error}`); alert(`Attempted to move ${disk} to ${body.storage} but got: ${result.error}`);
} }
refreshVolumes(); refreshVolumes();
refreshBoot(); refreshBoot();
@@ -141,7 +141,7 @@ class VolumeAction extends HTMLElement {
this.setStatusLoading(); this.setStatusLoading();
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/delete`, "DELETE"); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/delete`, "DELETE");
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to delete ${disk} but got: ${result.error}`); alert(`Attempted to delete ${disk} but got: ${result.error}`);
} }
refreshVolumes(); refreshVolumes();
refreshBoot(); refreshBoot();
@@ -162,7 +162,7 @@ async function initVolumes () {
async function refreshVolumes () { async function refreshVolumes () {
let volumes = await requestDash(`/config/volumes?node=${node}&type=${type}&vmid=${vmid}`, "GET"); let volumes = await requestDash(`/config/volumes?node=${node}&type=${type}&vmid=${vmid}`, "GET");
if (volumes.status !== 200) { if (volumes.status !== 200) {
error("Error fetching instance volumes."); alert("Error fetching instance volumes.");
} }
else { else {
volumes = volumes.data; volumes = volumes.data;
@@ -186,7 +186,7 @@ async function handleDiskAdd () {
const disk = `${prefix}${id}`; const disk = `${prefix}${id}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/create`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/create`, "POST", body);
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to create ${disk} but got: ${result.error}`); alert(`Attempted to create ${disk} but got: ${result.error}`);
} }
refreshVolumes(); refreshVolumes();
refreshBoot(); refreshBoot();
@@ -214,14 +214,14 @@ async function handleCDAdd () {
const disk = `ide${form.get("device")}`; const disk = `ide${form.get("device")}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/create`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/create`, "POST", body);
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to mount ${body.iso} to ${disk} but got: result.error`); alert(`Attempted to mount ${body.iso} to ${disk} but got: result.error`);
} }
refreshVolumes(); refreshVolumes();
refreshBoot(); refreshBoot();
} }
}); });
const isos = await requestAPI(`/access/users/${getCookie("username")}/vm-isos`, "GET"); const isos = await requestAPI("/user/vm-isos", "GET");
const select = d.querySelector("#iso-select"); const select = d.querySelector("#iso-select");
for (const iso of isos.data) { for (const iso of isos.data) {
@@ -263,7 +263,7 @@ class NetworkAction extends HTMLElement {
const net = `${netID}`; const net = `${netID}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/${net}/modify`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/${net}/modify`, "POST", body);
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to change ${net} but got: ${result.error}`); alert(`Attempted to change ${net} but got: ${result.error}`);
} }
refreshNetworks(); refreshNetworks();
refreshBoot(); refreshBoot();
@@ -281,7 +281,7 @@ class NetworkAction extends HTMLElement {
const net = `${netID}`; const net = `${netID}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/${net}/delete`, "DELETE"); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/${net}/delete`, "DELETE");
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to delete ${net} but got: ${result.error}`); alert(`Attempted to delete ${net} but got: ${result.error}`);
} }
refreshNetworks(); refreshNetworks();
refreshBoot(); refreshBoot();
@@ -299,7 +299,7 @@ async function initNetworks () {
async function refreshNetworks () { async function refreshNetworks () {
let nets = await requestDash(`/config/nets?node=${node}&type=${type}&vmid=${vmid}`, "GET"); let nets = await requestDash(`/config/nets?node=${node}&type=${type}&vmid=${vmid}`, "GET");
if (nets.status !== 200) { if (nets.status !== 200) {
error("Error fetching instance nets."); alert("Error fetching instance nets.");
} }
else { else {
nets = nets.data; nets = nets.data;
@@ -324,7 +324,7 @@ async function handleNetworkAdd () {
const net = `net${id}`; const net = `net${id}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/${net}/create`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/${net}/create`, "POST", body);
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to create ${net} but got: ${result.error}`); alert(`Attempted to create ${net} but got: ${result.error}`);
} }
refreshNetworks(); refreshNetworks();
refreshBoot(); refreshBoot();
@@ -367,7 +367,7 @@ class DeviceAction extends HTMLElement {
const device = `${deviceID}`; const device = `${deviceID}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/${device}/modify`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/${device}/modify`, "POST", body);
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to add ${device} but got: ${result.error}`); alert(`Attempted to add ${device} but got: ${result.error}`);
} }
refreshDevices(); refreshDevices();
} }
@@ -389,7 +389,7 @@ class DeviceAction extends HTMLElement {
const device = `${deviceID}`; const device = `${deviceID}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/${device}/delete`, "DELETE"); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/${device}/delete`, "DELETE");
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to delete ${device} but got: ${result.error}`); alert(`Attempted to delete ${device} but got: ${result.error}`);
} }
refreshDevices(); refreshDevices();
} }
@@ -408,7 +408,7 @@ async function initDevices () {
async function refreshDevices () { async function refreshDevices () {
let devices = await requestDash(`/config/devices?node=${node}&type=${type}&vmid=${vmid}`, "GET"); let devices = await requestDash(`/config/devices?node=${node}&type=${type}&vmid=${vmid}`, "GET");
if (devices.status !== 200) { if (devices.status !== 200) {
error("Error fetching instance devices."); alert("Error fetching instance devices.");
} }
else { else {
devices = devices.data; devices = devices.data;
@@ -431,7 +431,7 @@ async function handleDeviceAdd () {
const deviceID = `hostpci${hostpci}`; const deviceID = `hostpci${hostpci}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/${deviceID}/create`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/${deviceID}/create`, "POST", body);
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to add ${body.device} but got: ${result.error}`); alert(`Attempted to add ${body.device} but got: ${result.error}`);
} }
refreshDevices(); refreshDevices();
} }
@@ -447,12 +447,12 @@ async function handleDeviceAdd () {
async function refreshBoot () { async function refreshBoot () {
let boot = await requestDash(`/config/boot?node=${node}&type=${type}&vmid=${vmid}`, "GET"); let boot = await requestDash(`/config/boot?node=${node}&type=${type}&vmid=${vmid}`, "GET");
if (boot.status !== 200) { if (boot.status !== 200) {
error("Error fetching instance boot order."); alert("Error fetching instance boot order.");
} }
else if (type === "qemu") { else if (type === "qemu") {
boot = boot.data; boot = boot.data;
const container = document.querySelector("#boot-order"); const order = document.querySelector("#boot-order");
container.setHTMLUnsafe(boot); order.setHTMLUnsafe(boot);
} }
} }
@@ -474,6 +474,6 @@ async function handleFormExit (event) {
goToPage("index"); goToPage("index");
} }
else { else {
error(`Attempted to set basic resources but got: ${result.error}`); alert(`Attempted to set basic resources but got: ${result.error}`);
} }
} }
+31 -6
View File
@@ -41,6 +41,34 @@ export function dialog (template, onclose = async (_result, _form) => { }) {
return dialog; return dialog;
} }
export function alert (message) {
const dialog = document.querySelector("#alert-dialog");
if (dialog == null) {
const dialog = document.createElement("dialog");
dialog.id = "alert-dialog";
dialog.innerHTML = `
<form method="dialog">
<p class="w3-large" id="prompt">Alert</p>
<p class="w3-center" style="margin-bottom: 0;">${message}</p>
<div class="w3-center">
<button class="w3-button w3-margin" id="submit">OK</button>
</div>
</form>
`;
dialog.className = "w3-container w3-card w3-border-0";
document.body.append(dialog);
dialog.showModal();
dialog.addEventListener("close", () => {
dialog.parentElement.removeChild(dialog);
});
return dialog;
}
else {
console.error("Attempted to create a new alert while one already exists!");
return null;
}
}
class ErrorDialog extends HTMLElement { class ErrorDialog extends HTMLElement {
shadowRoot = null; shadowRoot = null;
dialog = null; dialog = null;
@@ -59,18 +87,15 @@ class ErrorDialog extends HTMLElement {
max-height: 20lh; max-height: 20lh;
min-height: 20lh; min-height: 20lh;
overflow-y: scroll; overflow-y: scroll;
padding-left: 12px;
padding-right: 12px;
} }
#errors * { #errors * {
margin: 0; margin: 0;
width: 100%;
} }
</style> </style>
<dialog class="w3-container w3-card w3-border-0"> <dialog class="w3-container w3-card w3-border-0">
<form method="dialog"> <form method="dialog">
<p class="w3-large" id="prompt">Error</p> <p class="w3-large" id="prompt">Error</p>
<div id="errors" class="flex column"></div> <div id="errors" class="flex column-reverse"></div>
<div class="w3-center" id="controls"> <div class="w3-center" id="controls">
<button class="w3-button w3-margin" type="submit" value="ok">OK</button> <button class="w3-button w3-margin" type="submit" value="ok">OK</button>
<button class="w3-button w3-margin" type="submit" value="copy">Copy</button> <button class="w3-button w3-margin" type="submit" value="copy">Copy</button>
@@ -96,7 +121,7 @@ class ErrorDialog extends HTMLElement {
} }
navigator.clipboard.writeText(errors); navigator.clipboard.writeText(errors);
} }
this.dialog.close(); this.parentElement.removeChild(this);
}); });
} }
@@ -116,7 +141,7 @@ customElements.define("error-dialog", ErrorDialog);
export function error (message) { export function error (message) {
let dialog = document.querySelector("error-dialog"); let dialog = document.querySelector("error-dialog");
if (dialog === null) { if (dialog == null) {
dialog = document.createElement("error-dialog"); dialog = document.createElement("error-dialog");
document.body.append(dialog); document.body.append(dialog);
dialog.appendError(message); dialog.appendError(message);
+14 -12
View File
@@ -1,16 +1,14 @@
import { requestPVE, requestAPI, setAppearance, getSetting, requestDash, setIconSrc, setIconAlt, getCookie } from "./utils.js"; import { requestPVE, requestAPI, setAppearance, getSetting, requestDash, setIconSrc, setIconAlt } from "./utils.js";
import { dialog, error } from "./dialog.js"; import { alert, dialog, error } from "./dialog.js";
import { setupClientSync } from "./clientsync.js"; import { setupClientSync } from "./clientsync.js";
import wfaInit from "../modules/wfa.js"; import wfaInit from "../modules/wfa.js";
window.addEventListener("DOMContentLoaded", init); window.addEventListener("DOMContentLoaded", init);
var wfa;
async function init () { async function init () {
setAppearance(); setAppearance();
wfa = await wfaInit("modules/wfa.wasm.gz"); wfaInit("modules/wfa.wasm");
initInstances(); initInstances();
document.querySelector("#instance-add").addEventListener("click", handleInstanceAddButton); document.querySelector("#instance-add").addEventListener("click", handleInstanceAddButton);
@@ -177,7 +175,7 @@ class InstanceCard extends HTMLElement {
break; break;
} }
else if (taskStatus.data.status === "stopped") { // task stopped but was not successful else if (taskStatus.data.status === "stopped") { // task stopped but was not successful
error(`Attempted to ${targetAction} ${this.vmid} but got: ${taskStatus.data.exitstatus}`); alert(`Attempted to ${targetAction} ${this.vmid} but got: ${taskStatus.data.exitstatus}`);
break; break;
} }
else { // task has not stopped else { // task has not stopped
@@ -205,7 +203,7 @@ class InstanceCard extends HTMLElement {
const result = await requestAPI(`/cluster/${this.node.name}/${this.type}/${this.vmid}/delete`, "DELETE"); const result = await requestAPI(`/cluster/${this.node.name}/${this.type}/${this.vmid}/delete`, "DELETE");
if (result.status !== 200) { if (result.status !== 200) {
error(`Attempted to delete ${this.vmid} but got: ${result.error}`); alert(`Attempted to delete ${this.vmid} but got: ${result.error}`);
} }
this.actionLock = false; this.actionLock = false;
@@ -218,8 +216,12 @@ class InstanceCard extends HTMLElement {
customElements.define("instance-card", InstanceCard); customElements.define("instance-card", InstanceCard);
async function getInstancesFragment () {
return await requestDash("/index/instances", "GET");
}
async function refreshInstances () { async function refreshInstances () {
let instances = await requestDash("/index/instances", "GET"); let instances = await getInstancesFragment();
if (instances.status !== 200) { if (instances.status !== 200) {
error(`Error fetching instances: ${instances.status} ${instances.error !== undefined ? instances.error : ""}`); error(`Error fetching instances: ${instances.status} ${instances.error !== undefined ? instances.error : ""}`);
} }
@@ -274,8 +276,8 @@ function sortInstances () {
}; };
criteria = (item, query) => { criteria = (item, query) => {
// lower is better // lower is better
const { score, CIGAR } = wfa.wfAlign(query, item, penalties, true); const { score, CIGAR } = global.wfa.wfAlign(query, item, penalties, true);
const alignment = wfa.DecodeCIGAR(CIGAR); const alignment = global.wfa.DecodeCIGAR(CIGAR);
return { score: score / item.length, alignment }; return { score: score / item.length, alignment };
}; };
} }
@@ -335,7 +337,7 @@ async function handleInstanceAddButton () {
refreshInstances(); refreshInstances();
} }
else { else {
error(`Attempted to create new instance ${vmid} but got: ${result.error}`); alert(`Attempted to create new instance ${vmid} but got: ${result.error}`);
refreshInstances(); refreshInstances();
} }
} }
@@ -418,7 +420,7 @@ async function handleInstanceAddButton () {
// setup templateImage depending on selected image storage // setup templateImage depending on selected image storage
const templateImage = d.querySelector("#template-image"); const templateImage = d.querySelector("#template-image");
// add template images to selector // add template images to selector
const templates = await requestAPI(`/access/users/${getCookie("username")}/ct-templates`, "GET"); const templates = await requestAPI("/user/ct-templates", "GET");
for (const template of templates.data) { for (const template of templates.data) {
templateImage.append(new Option(template.name, template.volid)); templateImage.append(new Option(template.name, template.volid));
} }
+6 -5
View File
@@ -1,12 +1,11 @@
import { goToPage, setAppearance, requestAPI } from "./utils.js"; import { goToPage, setAppearance, requestAPI } from "./utils.js";
import { error } from "./dialog.js"; import { alert } from "./dialog.js";
window.addEventListener("DOMContentLoaded", init); window.addEventListener("DOMContentLoaded", init);
async function init () { async function init () {
await deleteAllCookies(); await deleteAllCookies();
setAppearance(); setAppearance();
const formSubmitButton = document.querySelector("#submit"); const formSubmitButton = document.querySelector("#submit");
formSubmitButton.addEventListener("click", async (e) => { formSubmitButton.addEventListener("click", async (e) => {
e.preventDefault(); e.preventDefault();
@@ -20,16 +19,18 @@ async function init () {
goToPage("index"); goToPage("index");
} }
else if (ticket.status === 401) { else if (ticket.status === 401) {
error("Authenticaton failed."); alert("Authenticaton failed.");
formSubmitButton.innerText = "LOGIN"; formSubmitButton.innerText = "LOGIN";
} }
else if (ticket.status === 408) { else if (ticket.status === 408) {
error("Network error."); alert("Network error.");
formSubmitButton.innerText = "LOGIN"; formSubmitButton.innerText = "LOGIN";
} }
else { else {
error(`An error occured: ${JSON.stringify(ticket)}`); alert("An error occured.");
console.error(ticket);
formSubmitButton.innerText = "LOGIN"; formSubmitButton.innerText = "LOGIN";
console.error(ticket.error);
} }
}); });
} }
+1 -1
View File
@@ -30,7 +30,7 @@ function handleSettingsChange (event) {
saveBtn.classList.remove("disabled"); saveBtn.classList.remove("disabled");
saveBtn.classList.add("enabled"); saveBtn.classList.add("enabled");
} }
else if (getSetting("sync-rate") !== Number(form.get("sync-rate"))) { else if (getSetting("sync-rate") !== form.get("sync-rate")) {
saveBtn.classList.remove("disabled"); saveBtn.classList.remove("disabled");
saveBtn.classList.add("enabled"); saveBtn.classList.add("enabled");
} }
+1 -1
View File
@@ -5,7 +5,7 @@
<p id="nodes">Nodes: {{MapKeys .AllowedNodes ", "}}</p> <p id="nodes">Nodes: {{MapKeys .AllowedNodes ", "}}</p>
<p id="backups">Max Backups Per Instance: {{.AllowedBackups.MaxPerInstance}} Max Backups Total: {{.AllowedBackups.MaxTotal}}</p> <p id="backups">Max Backups Per Instance: {{.AllowedBackups.MaxPerInstance}} Max Backups Total: {{.AllowedBackups.MaxTotal}}</p>
<div> <div>
{{range $category, $v := .ResourceCharts}} {{range $category, $v := .Resources}}
{{if eq $category ""}} {{if eq $category ""}}
<h4>Generic</h4> <h4>Generic</h4>
{{else}} {{else}}