api: Cleanup websocket protocol

This commit cleans up the websocket protocol to be in line with the
documentation.
This commit is contained in:
Tom 2023-07-13 15:54:45 +02:00
parent 16fa721048
commit 1c68893a02
31 changed files with 3549 additions and 120 deletions

4
API.md
View file

@ -53,8 +53,8 @@ The server will always send exactly one of these frames before closing the conne
Text frames contain input and output from the underlying process. Text frames contain input and output from the underlying process.
They may be sent at any point after the client sends the initial `CallMessage` and before the final `ResultMessage`. They may be sent at any point after the client sends the initial `CallMessage` and before the final `ResultMessage`.
Each frame contains data for a single line, not including the line terminators. Each frame contains text input and output, including line separators.
Each input is sent directly to the underlying process. Input is sent directly to the underlying process.
### Supported Websocket Calls ### Supported Websocket Calls

View file

@ -1,4 +1,4 @@
.PHONY: clean all deps live tslint .PHONY: clean all deps live tslint tsfix
live: live:
sudo CGO_ENABLED=0 go run ./cmd/wdcli $(ARGS) sudo CGO_ENABLED=0 go run ./cmd/wdcli $(ARGS)
@ -12,6 +12,9 @@ wdcli:
tslint: tslint:
cd internal/dis/component/server/assets/ && yarn ts-standard cd internal/dis/component/server/assets/ && yarn ts-standard
tsfix:
cd internal/dis/component/server/assets/ && yarn ts-standard --fix
deps: internal/dis/component/server/assets/node_modules deps: internal/dis/component/server/assets/node_modules
internal/dis/component/server/assets/node_modules: internal/dis/component/server/assets/node_modules:

2
go.mod
View file

@ -16,7 +16,7 @@ require (
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.4.0
github.com/rs/zerolog v1.29.1 github.com/rs/zerolog v1.29.1
github.com/tkw1536/goprogram v0.3.5 github.com/tkw1536/goprogram v0.3.5
github.com/tkw1536/pkglib v0.0.0-20230629065114-9b97337c75a0 github.com/tkw1536/pkglib v0.0.0-20230713130635-2bcbc40ecdd9
github.com/yuin/goldmark v1.5.4 github.com/yuin/goldmark v1.5.4
github.com/yuin/goldmark-meta v1.1.0 github.com/yuin/goldmark-meta v1.1.0
golang.org/x/crypto v0.8.0 golang.org/x/crypto v0.8.0

4
go.sum
View file

@ -114,8 +114,8 @@ github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtp
github.com/tdewolff/test v1.0.7 h1:8Vs0142DmPFW/bQeHRP3MV19m1gvndjUb1sn8yy74LM= github.com/tdewolff/test v1.0.7 h1:8Vs0142DmPFW/bQeHRP3MV19m1gvndjUb1sn8yy74LM=
github.com/tkw1536/goprogram v0.3.5 h1:S0axKo3R/vGa4zhYqYDKAZEPhAfwUSSeMtVwnAu4sNY= github.com/tkw1536/goprogram v0.3.5 h1:S0axKo3R/vGa4zhYqYDKAZEPhAfwUSSeMtVwnAu4sNY=
github.com/tkw1536/goprogram v0.3.5/go.mod h1:pYr4dMHOSVurbPQ4KTR0ett8XWNISbsRS6zlh9Nsxa8= github.com/tkw1536/goprogram v0.3.5/go.mod h1:pYr4dMHOSVurbPQ4KTR0ett8XWNISbsRS6zlh9Nsxa8=
github.com/tkw1536/pkglib v0.0.0-20230629065114-9b97337c75a0 h1:M1iZRhxJtw9SZSGjowf1IpZx96U5fQK/c3uhK9vZOxk= github.com/tkw1536/pkglib v0.0.0-20230713130635-2bcbc40ecdd9 h1:l+xh6dn9PW+HNYrIXgwsOileuIhkdxZocOicQfBdB9M=
github.com/tkw1536/pkglib v0.0.0-20230629065114-9b97337c75a0/go.mod h1:0A1B9Cc5+yJXR3eeB14CqD4dFSbEjjWRo5Pr9M3XYuI= github.com/tkw1536/pkglib v0.0.0-20230713130635-2bcbc40ecdd9/go.mod h1:0A1B9Cc5+yJXR3eeB14CqD4dFSbEjjWRo5Pr9M3XYuI=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,36 @@
package scopes
import (
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
)
type Never struct {
component.Base
Dependencies struct {
Auth *auth.Auth
}
}
var (
_ component.ScopeProvider = (*Never)(nil)
)
const (
ScopeNever Scope = "never"
)
func (*Never) Scope() component.ScopeInfo {
return component.ScopeInfo{
Scope: ScopeNever,
Description: "scope that is never fullfilled",
DeniedMessage: "no one can do this",
TakesParam: false,
}
}
func (*Never) HasScope(string, *http.Request) (bool, error) {
return false, nil
}

View file

@ -84,14 +84,6 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http
router := httprouter.New() router := httprouter.New()
{
handler = &httpx.WebSocket{
Context: ctx,
Fallback: router,
Handler: admin.Dependencies.Sockets.Serve,
}
}
// add a handler for the index page // add a handler for the index page
{ {
index := admin.index(ctx) index := admin.index(ctx)
@ -161,7 +153,7 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http
// add a router for the login page // add a router for the login page
router.Handler(http.MethodPost, route+"login", admin.loginHandler(ctx)) router.Handler(http.MethodPost, route+"login", admin.loginHandler(ctx))
return return router, nil
} }
func (admin *Admin) loginHandler(ctx context.Context) http.Handler { func (admin *Admin) loginHandler(ctx context.Context) http.Handler {

View file

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/provision" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/provision"
"github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/FAU-CDI/wisski-distillery/internal/models"
@ -15,7 +16,7 @@ import (
func (sockets *Sockets) Actions() ActionMap { func (sockets *Sockets) Actions() ActionMap {
return map[string]Action{ return map[string]Action{
// generic actions // generic actions
"backup": sockets.Generic(0, func(ctx context.Context, sockets *Sockets, in io.Reader, out io.Writer, params ...string) error { "backup": sockets.Generic(scopes.ScopeUserAdmin, "", 0, func(ctx context.Context, sockets *Sockets, in io.Reader, out io.Writer, params ...string) error {
return sockets.Dependencies.Exporter.MakeExport( return sockets.Dependencies.Exporter.MakeExport(
ctx, ctx,
out, out,
@ -27,7 +28,7 @@ func (sockets *Sockets) Actions() ActionMap {
}, },
) )
}), }),
"provision": sockets.Generic(1, func(ctx context.Context, sockets *Sockets, in io.Reader, out io.Writer, params ...string) error { "provision": sockets.Generic(scopes.ScopeUserAdmin, "", 1, func(ctx context.Context, sockets *Sockets, in io.Reader, out io.Writer, params ...string) error {
// read the flags of the instance to be provisioned // read the flags of the instance to be provisioned
var flags provision.Flags var flags provision.Flags
if err := json.Unmarshal([]byte(params[0]), &flags); err != nil { if err := json.Unmarshal([]byte(params[0]), &flags); err != nil {
@ -52,7 +53,7 @@ func (sockets *Sockets) Actions() ActionMap {
// instance-specific actions! // instance-specific actions!
"snapshot": sockets.Instance(0, func(ctx context.Context, socket *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { "snapshot": sockets.Instance(scopes.ScopeUserAdmin, "", 0, func(ctx context.Context, socket *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error {
return socket.Dependencies.Exporter.MakeExport( return socket.Dependencies.Exporter.MakeExport(
ctx, ctx,
out, out,
@ -64,7 +65,7 @@ func (sockets *Sockets) Actions() ActionMap {
}, },
) )
}), }),
"rebuild": sockets.Instance(1, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { "rebuild": sockets.Instance(scopes.ScopeUserAdmin, "", 1, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error {
// read the flags of the instance to be provisioned // read the flags of the instance to be provisioned
var system models.System var system models.System
if err := json.Unmarshal([]byte(params[0]), &system); err != nil { if err := json.Unmarshal([]byte(params[0]), &system); err != nil {
@ -72,20 +73,23 @@ func (sockets *Sockets) Actions() ActionMap {
} }
return instance.SystemManager().Apply(ctx, out, system, true) return instance.SystemManager().Apply(ctx, out, system, true)
}), }),
"update": sockets.Instance(0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { "update": sockets.Instance(scopes.ScopeUserAdmin, "", 0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error {
return instance.Drush().Update(ctx, out) return instance.Drush().Update(ctx, out)
}), }),
"cron": sockets.Instance(0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, str io.Writer, params ...string) error { "cron": sockets.Instance(scopes.ScopeUserAdmin, "", 0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, str io.Writer, params ...string) error {
return instance.Drush().Cron(ctx, str) return instance.Drush().Cron(ctx, str)
}), }),
"start": sockets.Instance(0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { "start": sockets.Instance(scopes.ScopeUserAdmin, "", 0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error {
return instance.Barrel().Stack().Up(ctx, out) return instance.Barrel().Stack().Up(ctx, out)
}), }),
"stop": sockets.Instance(0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { "stop": sockets.Instance(scopes.ScopeUserAdmin, "", 0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error {
return instance.Barrel().Stack().Down(ctx, out) return instance.Barrel().Stack().Down(ctx, out)
}), }),
"purge": sockets.Instance(0, func(ctx context.Context, sockets *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { "purge": sockets.Instance(scopes.ScopeUserAdmin, "", 0, func(ctx context.Context, sockets *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error {
return sockets.Dependencies.Purger.Purge(ctx, out, instance.Slug) return sockets.Dependencies.Purger.Purge(ctx, out, instance.Slug)
}), }),
"never": sockets.Generic(scopes.ScopeNever, "", 0, func(ctx context.Context, sockets *Sockets, in io.Reader, out io.Writer, params ...string) error {
panic("never called")
}),
} }
} }

View file

@ -9,9 +9,11 @@ import (
"sync" "sync"
"time" "time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/tkw1536/pkglib/httpx" "github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/status"
) )
// ActionMap handles a set of WebSocket actions // ActionMap handles a set of WebSocket actions
@ -27,7 +29,7 @@ func (err errPanic) Error() string {
return fmt.Sprintf("fatal error: %v", err.value) return fmt.Sprintf("fatal error: %v", err.value)
} }
// Handle handles a new incoming websocket connection. // Handle handles a new incoming websocket connection using the given authentication.
// //
// There are two kinds of messages: // There are two kinds of messages:
// //
@ -40,7 +42,7 @@ func (err errPanic) Error() string {
// Finally it will send a ResultMessage once handling is complete. // Finally it will send a ResultMessage once handling is complete.
// //
// A corresponding client implementation of this can be found in ..../remote/proto.ts // A corresponding client implementation of this can be found in ..../remote/proto.ts
func (am ActionMap) Handle(conn httpx.WebSocketConnection) (name string, err error) { func (am ActionMap) Handle(auth *auth.Auth, conn httpx.WebSocketConnection) (name string, err error) {
var wg sync.WaitGroup var wg sync.WaitGroup
// once we have finished executing send a binary message (indicating success) to the client. // once we have finished executing send a binary message (indicating success) to the client.
@ -123,12 +125,17 @@ func (am ActionMap) Handle(conn httpx.WebSocketConnection) (name string, err err
// check that the given action exists! // check that the given action exists!
// and has the right number of parameters! // and has the right number of parameters!
action, ok := am[call.Name] action, ok := am[call.Call]
if !ok || action.Handle == nil { if !ok || action.Handle == nil {
return call.Name, errUnknownAction return call.Call, errUnknownAction
} }
if action.NumParams != len(call.Params) { if action.NumParams != len(call.Params) {
return call.Name, errIncorrectParams return call.Call, errIncorrectParams
}
// check that we have the given permission
if err := auth.CheckScope(action.ScopeParam, action.scope(), conn.Request()); err != nil {
return call.Call, err
} }
// create a context to be canceled once done // create a context to be canceled once done
@ -170,17 +177,22 @@ func (am ActionMap) Handle(conn httpx.WebSocketConnection) (name string, err err
} }
}() }()
// create a linebuffer to write the output line by line // write the output to the client as it comes in!
output := &status.LineBuffer{ // NOTE(twiesing): We may eventually need buffering here ...
Line: func(line string) { output := WriteFunc(func(b []byte) (int, error) {
<-conn.WriteText(line) <-conn.WriteText(string(b))
}, return len(b), nil
FlushLineOnClose: true, })
}
defer output.Close()
// handle the actual // handle the actual
return call.Name, action.Handle(ctx, inputR, output, call.Params...) return call.Call, action.Handle(ctx, inputR, output, call.Params...)
}
// WriteFunc implements io.Writer using a function.
type WriteFunc func([]byte) (int, error)
func (wf WriteFunc) Write(b []byte) (int, error) {
return wf(b)
} }
// Action is something that can be handled via a WebSocket connection. // Action is something that can be handled via a WebSocket connection.
@ -188,6 +200,11 @@ type Action struct {
// NumPara // NumPara
NumParams int NumParams int
// Scope and ScopeParam indicate the scope required by the caller.
// TODO(twiesing): Once we actually include scopes, make them dynamic
Scope component.Scope
ScopeParam string
// Handle handles this action. // Handle handles this action.
// //
// ctx is closed once the underlying connection is closed. // ctx is closed once the underlying connection is closed.
@ -196,9 +213,18 @@ type Action struct {
Handle func(ctx context.Context, in io.Reader, out io.Writer, params ...string) error Handle func(ctx context.Context, in io.Reader, out io.Writer, params ...string) error
} }
// scope returns the actual scope required by this action.
// If the caller did not provide an actual scope, uses ScopeNever
func (action Action) scope() component.Scope {
if action.Scope == "" {
return scopes.ScopeNever
}
return action.Scope
}
// CallMessage is sent by the client to the server to invoke a remote procedure // CallMessage is sent by the client to the server to invoke a remote procedure
type CallMessage struct { type CallMessage struct {
Name string `json:"name"` Call string `json:"call"`
Params []string `json:"params,omitempty"` Params []string `json:"params,omitempty"`
} }

View file

@ -3,8 +3,11 @@ package socket
import ( import (
"context" "context"
"io" "io"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances/purger" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances/purger"
@ -25,22 +28,44 @@ type Sockets struct {
Instances *instances.Instances Instances *instances.Instances
Exporter *exporter.Exporter Exporter *exporter.Exporter
Purger *purger.Purger Purger *purger.Purger
Auth *auth.Auth
} }
} }
var (
_ component.Routeable = (*Sockets)(nil)
)
func (socket *Sockets) Routes() component.Routes {
return component.Routes{
Prefix: "/api/v1/ws",
Exact: true,
Decorator: socket.Dependencies.Auth.Require(true, scopes.ScopeUserValid, nil),
}
}
func (sockets *Sockets) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
return &httpx.WebSocket{
Context: ctx,
Handler: sockets.Serve,
}, nil
}
// Serve handles a connection to the websocket api // Serve handles a connection to the websocket api
func (socket *Sockets) Serve(conn httpx.WebSocketConnection) { func (socket *Sockets) Serve(conn httpx.WebSocketConnection) {
// handle the websocket connection! // handle the websocket connection!
name, err := socket.actions.Get(socket.Actions).Handle(conn) name, err := socket.actions.Get(socket.Actions).Handle(socket.Dependencies.Auth, conn)
if err != nil { if err != nil {
zerolog.Ctx(conn.Context()).Err(err).Str("name", name).Msg("Error handling websocket") zerolog.Ctx(conn.Context()).Err(err).Str("name", name).Msg("Error handling websocket")
} }
} }
// Generic returns a new action that calls handler with the provided number of parameters // Generic returns a new action that calls handler with the provided number of parameters
func (sockets *Sockets) Generic(numParams int, handler func(ctx context.Context, socket *Sockets, in io.Reader, out io.Writer, params ...string) error) Action { func (sockets *Sockets) Generic(scope component.Scope, scopeParam string, numParams int, handler func(ctx context.Context, socket *Sockets, in io.Reader, out io.Writer, params ...string) error) Action {
return Action{ return Action{
NumParams: numParams, Scope: scope,
ScopeParam: scopeParam,
NumParams: numParams,
Handle: func(ctx context.Context, in io.Reader, out io.Writer, params ...string) error { Handle: func(ctx context.Context, in io.Reader, out io.Writer, params ...string) error {
return handler(ctx, sockets, in, out, params...) return handler(ctx, sockets, in, out, params...)
}, },
@ -48,8 +73,11 @@ func (sockets *Sockets) Generic(numParams int, handler func(ctx context.Context,
} }
// Insstance returns a new action that calls handler with a specific WissKI instance // Insstance returns a new action that calls handler with a specific WissKI instance
func (sockets *Sockets) Instance(numParams int, handler func(ctx context.Context, sockets *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error) Action { func (sockets *Sockets) Instance(scope component.Scope, scopeParam string, numParams int, handler func(ctx context.Context, sockets *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error) Action {
return Action{ return Action{
Scope: scope,
ScopeParam: scopeParam,
NumParams: numParams + 1, NumParams: numParams + 1,
Handle: func(ctx context.Context, in io.Reader, out io.Writer, params ...string) error { Handle: func(ctx context.Context, in io.Reader, out io.Writer, params ...string) error {
instance, err := sockets.Dependencies.Instances.WissKI(ctx, params[0]) instance, err := sockets.Dependencies.Instances.WissKI(ctx, params[0])

File diff suppressed because it is too large Load diff

View file

@ -18,24 +18,24 @@ var AssetsDefault = Assets{
// AssetsUser contains assets for the 'User' entrypoint. // AssetsUser contains assets for the 'User' entrypoint.
var AssetsUser = Assets{ var AssetsUser = Assets{
Scripts: `<script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/User.47a3b7e3.js"></script><script src="/⛰/User.924f7900.js" nomodule="" defer></script>`, Scripts: `<script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/User.fce9a3e3.js"></script><script src="/⛰/User.e4c5f849.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css">`, Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css">`,
} }
// AssetsAdmin contains assets for the 'Admin' entrypoint. // AssetsAdmin contains assets for the 'Admin' entrypoint.
var AssetsAdmin = Assets{ var AssetsAdmin = Assets{
Scripts: `<script nomodule="" defer src="/⛰/User.924f7900.js"></script><script type="module" src="/⛰/User.47a3b7e3.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/Admin.ad1b495b.js"></script><script src="/⛰/Admin.6daf9fdd.js" nomodule="" defer></script>`, Scripts: `<script nomodule="" defer src="/⛰/User.e4c5f849.js"></script><script type="module" src="/⛰/User.fce9a3e3.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/Admin.87f202f8.js"></script><script src="/⛰/Admin.1b10eebb.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/Admin.a1e05c23.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/Admin.d79c7b30.css">`, Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/Admin.a1e05c23.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/Admin.d79c7b30.css">`,
} }
// AssetsAdminProvision contains assets for the 'AdminProvision' entrypoint. // AssetsAdminProvision contains assets for the 'AdminProvision' entrypoint.
var AssetsAdminProvision = Assets{ var AssetsAdminProvision = Assets{
Scripts: `<script nomodule="" defer src="/⛰/User.924f7900.js"></script><script nomodule="" defer src="/⛰/Admin.6daf9fdd.js"></script><script type="module" src="/⛰/User.47a3b7e3.js"></script><script type="module" src="/⛰/Admin.ad1b495b.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/AdminProvision.b7679968.js"></script><script src="/⛰/AdminProvision.12a47f22.js" nomodule="" defer></script>`, Scripts: `<script nomodule="" defer src="/⛰/User.e4c5f849.js"></script><script nomodule="" defer src="/⛰/Admin.1b10eebb.js"></script><script type="module" src="/⛰/User.fce9a3e3.js"></script><script type="module" src="/⛰/Admin.87f202f8.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/AdminProvision.9cbda41c.js"></script><script src="/⛰/AdminProvision.68dbff79.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/Admin.a1e05c23.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/Admin.d79c7b30.css"><link rel="stylesheet" href="/⛰/AdminProvision.38d394c2.css">`, Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/Admin.a1e05c23.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/Admin.d79c7b30.css"><link rel="stylesheet" href="/⛰/AdminProvision.38d394c2.css">`,
} }
// AssetsAdminRebuild contains assets for the 'AdminRebuild' entrypoint. // AssetsAdminRebuild contains assets for the 'AdminRebuild' entrypoint.
var AssetsAdminRebuild = Assets{ var AssetsAdminRebuild = Assets{
Scripts: `<script nomodule="" defer src="/⛰/User.924f7900.js"></script><script nomodule="" defer src="/⛰/Admin.6daf9fdd.js"></script><script type="module" src="/⛰/User.47a3b7e3.js"></script><script type="module" src="/⛰/Admin.ad1b495b.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/AdminRebuild.330247d9.js"></script><script src="/⛰/AdminRebuild.527a9616.js" nomodule="" defer></script>`, Scripts: `<script nomodule="" defer src="/⛰/User.e4c5f849.js"></script><script nomodule="" defer src="/⛰/Admin.1b10eebb.js"></script><script type="module" src="/⛰/User.fce9a3e3.js"></script><script type="module" src="/⛰/Admin.87f202f8.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/AdminRebuild.0149f285.js"></script><script src="/⛰/AdminRebuild.6953ed8a.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/Admin.a1e05c23.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/Admin.d79c7b30.css"><link rel="stylesheet" href="/⛰/AdminRebuild.38d394c2.css">`, Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/Admin.a1e05c23.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/Admin.d79c7b30.css"><link rel="stylesheet" href="/⛰/AdminRebuild.38d394c2.css">`,
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
!function(){var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},o={},t=e.parcelRequireafa4;null==t&&((t=function(e){if(e in n)return n[e].exports;if(e in o){var t=o[e];delete o[e];var r={id:e,exports:{}};return n[e]=r,t.call(r.exports,r,r.exports),r.exports}var l=new Error("Cannot find module '"+e+"'");throw l.code="MODULE_NOT_FOUND",l}).register=function(e,n){o[e]=n},e.parcelRequireafa4=t),t("dK5Bi");var r,l=t("8vh0V");async function i(e){return new Promise(((n,o)=>{(0,l.createModal)("provision",[JSON.stringify(e)],{bufferSize:0,onClose:(t,r)=>{t?n(e.Slug):o(new Error(null!=r?r:"unspecified error"))}})}))}const d=document.getElementById("provision"),a=document.getElementById("slug"),u=document.getElementById("php"),c=document.getElementById("opcacheDevelopment");d.addEventListener("submit",(e=>{e.preventDefault(),i({Slug:a.value,System:{PHP:u.value,OpCacheDevelopment:c.checked}}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),null===(r=d.querySelector("fieldset"))||void 0===r||r.removeAttribute("disabled")}();

View file

@ -0,0 +1 @@
!function(){var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},t={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in t){var o=t[e];delete t[e];var r={id:e,exports:{}};return n[e]=r,o.call(r.exports,r,r.exports),r.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,n){t[e]=n},e.parcelRequireafa4=o),o("dK5Bi");var r,i=o("8vh0V");async function l(e){return await new Promise(((n,t)=>{(0,i.createModal)("provision",[JSON.stringify(e)],{bufferSize:0,onClose:(o,r)=>{o?n(e.Slug):t(new Error(null!=r?r:"unspecified error"))}})}))}const a=document.getElementById("provision"),d=document.getElementById("slug"),u=document.getElementById("php"),c=document.getElementById("opcacheDevelopment");a.addEventListener("submit",(e=>{e.preventDefault(),l({Slug:d.value,System:{PHP:u.value,OpCacheDevelopment:c.checked}}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),null===(r=a.querySelector("fieldset"))||void 0===r||r.removeAttribute("disabled")}();

View file

@ -0,0 +1 @@
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},t={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in t){var o=t[e];delete t[e];var r={id:e,exports:{}};return n[e]=r,o.call(r.exports,r,r.exports),r.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,n){t[e]=n},e.parcelRequireafa4=o),o("8xGhL");var r=o("12vpF");async function i(e){return await new Promise(((n,t)=>{(0,r.createModal)("provision",[JSON.stringify(e)],{bufferSize:0,onClose:(o,r)=>{o?n(e.Slug):t(new Error(r??"unspecified error"))}})}))}const l=document.getElementById("provision"),a=document.getElementById("slug"),d=document.getElementById("php"),u=document.getElementById("opcacheDevelopment");l.addEventListener("submit",(e=>{e.preventDefault(),i({Slug:a.value,System:{PHP:d.value,OpCacheDevelopment:u.checked}}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),l.querySelector("fieldset")?.removeAttribute("disabled");

View file

@ -1 +0,0 @@
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},t={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in t){var o=t[e];delete t[e];var r={id:e,exports:{}};return n[e]=r,o.call(r.exports,r,r.exports),r.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,n){t[e]=n},e.parcelRequireafa4=o),o("8xGhL");var r=o("12vpF");async function i(e){return new Promise(((n,t)=>{(0,r.createModal)("provision",[JSON.stringify(e)],{bufferSize:0,onClose:(o,r)=>{o?n(e.Slug):t(new Error(r??"unspecified error"))}})}))}const l=document.getElementById("provision"),a=document.getElementById("slug"),d=document.getElementById("php"),u=document.getElementById("opcacheDevelopment");l.addEventListener("submit",(e=>{e.preventDefault(),i({Slug:a.value,System:{PHP:d.value,OpCacheDevelopment:u.checked}}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),l.querySelector("fieldset")?.removeAttribute("disabled");

View file

@ -0,0 +1 @@
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},t={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in t){var o=t[e];delete t[e];var r={id:e,exports:{}};return n[e]=r,o.call(r.exports,r,r.exports),r.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,n){t[e]=n},e.parcelRequireafa4=o),o("8xGhL");var r=o("12vpF");async function i(e,n){return await new Promise(((t,o)=>{(0,r.createModal)("rebuild",[e,JSON.stringify(n)],{bufferSize:0,onClose:(n,r)=>{n?t(e):o(new Error(r??"unspecified error"))}})}))}const l=document.getElementById("slug"),a=document.getElementById("provision"),d=document.getElementById("php"),u=document.getElementById("opcacheDevelopment");a.addEventListener("submit",(e=>{e.preventDefault(),i(l.value,{PHP:d.value,OpCacheDevelopment:u.checked}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),a.querySelector("fieldset")?.removeAttribute("disabled");

View file

@ -1 +0,0 @@
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},t={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in t){var o=t[e];delete t[e];var r={id:e,exports:{}};return n[e]=r,o.call(r.exports,r,r.exports),r.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,n){t[e]=n},e.parcelRequireafa4=o),o("8xGhL");var r=o("12vpF");async function i(e,n){return new Promise(((t,o)=>{(0,r.createModal)("rebuild",[e,JSON.stringify(n)],{bufferSize:0,onClose:(n,r)=>{n?t(e):o(new Error(r??"unspecified error"))}})}))}const l=document.getElementById("slug"),d=document.getElementById("provision"),a=document.getElementById("php"),u=document.getElementById("opcacheDevelopment");d.addEventListener("submit",(e=>{e.preventDefault(),i(l.value,{PHP:a.value,OpCacheDevelopment:u.checked}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),d.querySelector("fieldset")?.removeAttribute("disabled");

View file

@ -1 +0,0 @@
!function(){var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},t={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in t){var o=t[e];delete t[e];var r={id:e,exports:{}};return n[e]=r,o.call(r.exports,r,r.exports),r.exports}var l=new Error("Cannot find module '"+e+"'");throw l.code="MODULE_NOT_FOUND",l}).register=function(e,n){t[e]=n},e.parcelRequireafa4=o),o("dK5Bi");var r,l=o("8vh0V");async function i(e,n){return new Promise(((t,o)=>{(0,l.createModal)("rebuild",[e,JSON.stringify(n)],{bufferSize:0,onClose:(n,r)=>{n?t(e):o(new Error(null!=r?r:"unspecified error"))}})}))}const d=document.getElementById("slug"),a=document.getElementById("provision"),u=document.getElementById("php"),c=document.getElementById("opcacheDevelopment");a.addEventListener("submit",(e=>{e.preventDefault(),i(d.value,{PHP:u.value,OpCacheDevelopment:c.checked}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),null===(r=a.querySelector("fieldset"))||void 0===r||r.removeAttribute("disabled")}();

View file

@ -0,0 +1 @@
!function(){var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},t={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in t){var o=t[e];delete t[e];var r={id:e,exports:{}};return n[e]=r,o.call(r.exports,r,r.exports),r.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,n){t[e]=n},e.parcelRequireafa4=o),o("dK5Bi");var r,i=o("8vh0V");async function l(e,n){return await new Promise(((t,o)=>{(0,i.createModal)("rebuild",[e,JSON.stringify(n)],{bufferSize:0,onClose:(n,r)=>{n?t(e):o(new Error(null!=r?r:"unspecified error"))}})}))}const d=document.getElementById("slug"),a=document.getElementById("provision"),u=document.getElementById("php"),c=document.getElementById("opcacheDevelopment");a.addEventListener("submit",(e=>{e.preventDefault(),l(d.value,{PHP:u.value,OpCacheDevelopment:c.checked}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),null===(r=a.querySelector("fieldset"))||void 0===r||r.removeAttribute("disabled")}();

View file

@ -1 +0,0 @@
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},t={},n={},r=e.parcelRequireafa4;null==r&&((r=function(e){if(e in t)return t[e].exports;if(e in n){var r=n[e];delete n[e];var o={id:e,exports:{}};return t[e]=o,r.call(o.exports,o,o.exports),o.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,t){n[e]=t},e.parcelRequireafa4=r),r.register("gkpdw",(function(e,t){r("hZNgY"),r("lFehX")})),r.register("hZNgY",(function(e,t){document.querySelectorAll(".copy").forEach((e=>{e.addEventListener("click",(()=>{navigator.clipboard&&navigator.clipboard.writeText(e.innerText)}))}))})),r.register("lFehX",(function(e,t){document.querySelectorAll("span").forEach((e=>{e.hasAttribute("data-reveal")&&function(e,t){const n=e.getAttribute("data-reveal")??"(no content)";let r=!0;const o=()=>{r=!0,e.innerText="(click to reveal)"};o();const i=()=>{r=!1;const t=document.createElement("code");t.append(n),t.addEventListener("click",(e=>{e.preventDefault(),navigator.clipboard&&navigator.clipboard.writeText(n)})),t.style.userSelect="all",e.innerHTML="",e.append(t)};e.addEventListener("click",(e=>{e.preventDefault(),r&&(i(),setTimeout(o,t))}))}(e,1e4)}))})),r("gkpdw");

View file

@ -1 +0,0 @@
!function(){var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},t={},n={},r=e.parcelRequireafa4;null==r&&((r=function(e){if(e in t)return t[e].exports;if(e in n){var r=n[e];delete n[e];var o={id:e,exports:{}};return t[e]=o,r.call(o.exports,o,o.exports),o.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,t){n[e]=t},e.parcelRequireafa4=r),r.register("kEAtK",(function(e,t){r("15EWx"),r("fp21h")})),r.register("15EWx",(function(e,t){document.querySelectorAll(".copy").forEach((e=>{e.addEventListener("click",(()=>{navigator.clipboard&&navigator.clipboard.writeText(e.innerText)}))}))})),r.register("fp21h",(function(e,t){document.querySelectorAll("span").forEach((e=>{e.hasAttribute("data-reveal")&&function(e,t){var n;const r=null!==(n=e.getAttribute("data-reveal"))&&void 0!==n?n:"(no content)";let o=!0;const i=()=>{o=!0,e.innerText="(click to reveal)"};i();const a=()=>{o=!1;const t=document.createElement("code");t.append(r),t.addEventListener("click",(e=>{e.preventDefault(),navigator.clipboard&&navigator.clipboard.writeText(r)})),t.style.userSelect="all",e.innerHTML="",e.append(t)};e.addEventListener("click",(e=>{e.preventDefault(),o&&(a(),setTimeout(i,t))}))}(e,1e4)}))})),r("kEAtK")}();

View file

@ -0,0 +1 @@
!function(){function e(e,t,n,r){Object.defineProperty(e,t,{get:n,set:r,enumerable:!0,configurable:!0})}var t="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},r={},o=t.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in r){var t=r[e];delete r[e];var o={id:e,exports:{}};return n[e]=o,t.call(o.exports,o,o.exports),o.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,t){r[e]=t},t.parcelRequireafa4=o),o.register("kEAtK",(function(e,t){o("15EWx"),o("fp21h")})),o.register("15EWx",(function(e,t){var n=o("17GEA");document.querySelectorAll(".copy").forEach((e=>{e.addEventListener("click",(()=>{navigator.clipboard&&(0,n.discard)(navigator.clipboard.writeText(e.innerText))}))}))})),o.register("17GEA",(function(t,n){e(t.exports,"discard",(function(){return o})),e(t.exports,"runMutexExclusive",(function(){return i}));const r=console.error.bind(console);function o(e){e.then((()=>{})).catch(r)}function i(e,t){o(e.runExclusive(t))}})),o.register("fp21h",(function(e,t){document.querySelectorAll("span").forEach((e=>{e.hasAttribute("data-reveal")&&function(e,t){var n;const r=null!==(n=e.getAttribute("data-reveal"))&&void 0!==n?n:"(no content)";let o=!0;const i=()=>{o=!0,e.innerText="(click to reveal)"};i();const c=()=>{o=!1;const t=document.createElement("code");t.append(r),t.addEventListener("click",(e=>{e.preventDefault(),navigator.clipboard&&navigator.clipboard.writeText(r).then((()=>{})).catch(console.error.bind(console))})),t.style.userSelect="all",e.innerHTML="",e.append(t)};e.addEventListener("click",(e=>{e.preventDefault(),o&&(c(),setTimeout(i,t))}))}(e,1e4)}))})),o("kEAtK")}();

View file

@ -0,0 +1 @@
function e(e,t,n,r){Object.defineProperty(e,t,{get:n,set:r,enumerable:!0,configurable:!0})}var t="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},r={},o=t.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in r){var t=r[e];delete r[e];var o={id:e,exports:{}};return n[e]=o,t.call(o.exports,o,o.exports),o.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,t){r[e]=t},t.parcelRequireafa4=o),o.register("gkpdw",(function(e,t){o("hZNgY"),o("lFehX")})),o.register("hZNgY",(function(e,t){var n=o("7E9W8");document.querySelectorAll(".copy").forEach((e=>{e.addEventListener("click",(()=>{navigator.clipboard&&(0,n.discard)(navigator.clipboard.writeText(e.innerText))}))}))})),o.register("7E9W8",(function(t,n){e(t.exports,"discard",(function(){return o})),e(t.exports,"runMutexExclusive",(function(){return i}));const r=console.error.bind(console);function o(e){e.then((()=>{})).catch(r)}function i(e,t){o(e.runExclusive(t))}})),o.register("lFehX",(function(e,t){document.querySelectorAll("span").forEach((e=>{e.hasAttribute("data-reveal")&&function(e,t){const n=e.getAttribute("data-reveal")??"(no content)";let r=!0;const o=()=>{r=!0,e.innerText="(click to reveal)"};o();const i=()=>{r=!1;const t=document.createElement("code");t.append(n),t.addEventListener("click",(e=>{e.preventDefault(),navigator.clipboard&&navigator.clipboard.writeText(n).then((()=>{})).catch(console.error.bind(console))})),t.style.userSelect="all",e.innerHTML="",e.append(t)};e.addEventListener("click",(e=>{e.preventDefault(),r&&(i(),setTimeout(o,t))}))}(e,1e4)}))})),o("gkpdw");

View file

@ -1,36 +1,63 @@
import './index.css' import './index.css'
import callServerAction, { ResultMessage } from './proto' import callServerAction, { ResultMessage } from './proto'
type Println = ((line: string, flush?: boolean) => void) & { type Print = ((text: string, flush?: boolean) => void) & {
paintedFrames: number paintedFrames: number
missedFrames: number missedFrames: number
} }
const NEW_LINE = '\n'
const NEW_LINE_LENGTH = NEW_LINE.length
/**
* trimLines trims buffer so that it contains as most count lines
*/
function trimLines (buffer: string, lines: number): string {
if (lines <= 0 || isNaN(lines) || !isFinite(lines)) return buffer
let count = 0
let index = buffer.length
// while we still have sufficient space
while (count < lines) {
// get the next start of the line
index = buffer.lastIndexOf(NEW_LINE, index - 1)
if (index === -1) {
return buffer
}
// increase the count
count++
}
return buffer.substring(index + NEW_LINE_LENGTH)
}
/** /**
* makeTextBuffer returns a println() function that efficiently writes text into target, and keeps at most size elements in the traceback. * makeTextBuffer returns a println() function that efficiently writes text into target, and keeps at most size elements in the traceback.
* scrollContainer is used to scroll on every painted update. * scrollContainer is used to scroll on every painted update.
*/ */
function makeTextBuffer (target: HTMLElement, scrollContainer: HTMLElement, size: number): Println { function makeTextBuffer (target: HTMLElement, scrollContainer: HTMLElement, size: number): Print {
let lastAnimationFrame: number | null = null // last scheduled animation frame let lastAnimationFrame: number | null = null // last scheduled animation frame
const buffer: string[] = [] // the internal buffer of lines // text buffer
let buffer: string = ''
const paint = (): void => { const paint = (): void => {
println.paintedFrames++ print.paintedFrames++
target.innerText = buffer.join('\n') target.innerText = buffer
scrollContainer.scrollTop = scrollContainer.scrollHeight scrollContainer.scrollTop = scrollContainer.scrollHeight
lastAnimationFrame = null lastAnimationFrame = null
} }
const println = (line: string, flush?: boolean): void => { const print = (text: string, flush?: boolean): void => {
// add the line // add text to the buffer and normalize
buffer.push(line) buffer += text.replace(/^\s*[\r\n]/gm, '\r\n')
if (size !== 0 && buffer.length > size) {
buffer.splice(0, buffer.length - size)
}
// trim the buffer to the specified number of lines
buffer = trimLines(buffer, size)
// and update the browser in the next animation frame // and update the browser in the next animation frame
if (lastAnimationFrame !== null) { if (lastAnimationFrame !== null) {
println.missedFrames++ print.missedFrames++
window.cancelAnimationFrame(lastAnimationFrame) window.cancelAnimationFrame(lastAnimationFrame)
} }
@ -40,10 +67,10 @@ function makeTextBuffer (target: HTMLElement, scrollContainer: HTMLElement, size
// schedule an animation frame // schedule an animation frame
lastAnimationFrame = window.requestAnimationFrame(paint) lastAnimationFrame = window.requestAnimationFrame(paint)
} }
println.paintedFrames = 0 print.paintedFrames = 0
println.missedFrames = 0 print.missedFrames = 0
return println return print
} }
export default function setup (): void { export default function setup (): void {
@ -131,7 +158,7 @@ export function createModal (action: string, params: string[], opts: Partial<Mod
// create a <pre> to write stuff into // create a <pre> to write stuff into
const target = document.createElement('pre') const target = document.createElement('pre')
const println = makeTextBuffer(target, modal, opts.bufferSize ?? 1000) const print = makeTextBuffer(target, modal, opts.bufferSize ?? 1000)
modal.append(target) modal.append(target)
// create a button to eventually close everything // create a button to eventually close everything
@ -171,9 +198,9 @@ export function createModal (action: string, params: string[], opts: Partial<Mod
result = message result = message
if (result.success) { if (result.success) {
println('Process completed successfully. ', true) print('Process completed successfully.\n', true)
} else { } else {
println('Process reported error: ' + result.message, true) print('Process reported error: ' + result.message + '\n', true)
} }
window.onbeforeunload = onbeforeunload window.onbeforeunload = onbeforeunload
@ -181,17 +208,20 @@ export function createModal (action: string, params: string[], opts: Partial<Mod
modal.removeChild(cancelButton) modal.removeChild(cancelButton)
modal.append(finishButton) modal.append(finishButton)
const quota = (println.paintedFrames / (println.missedFrames + println.paintedFrames)) * 100 const quota = (print.paintedFrames / (print.missedFrames + print.paintedFrames)) * 100
console.debug(`Terminal: painted=${println.paintedFrames} missed=${println.missedFrames} (${quota}%)`, true) console.debug(`Terminal: painted=${print.paintedFrames} missed=${print.missedFrames} (${quota}%)`, true)
} }
println('Connecting ...', true) print('Connecting ...', true)
// backendURL is the backend url to connect to
const backendURL = location.protocol.replace('http', 'ws') + '//' + location.host + '/api/v1/ws'
// connect to the socket and send the action // connect to the socket and send the action
callServerAction( callServerAction(
location.href.replace('http', 'ws'), backendURL,
{ {
name: action, call: action,
params params
}, },
( (
@ -202,12 +232,12 @@ export function createModal (action: string, params: string[], opts: Partial<Mod
cancelButton.addEventListener('click', (event) => { cancelButton.addEventListener('click', (event) => {
event.preventDefault() event.preventDefault()
println('Cancelling', true) print('^C\n', true)
cancel() cancel()
}) })
println('Connected', true) print(' Connected.\n', true)
}, },
println print
).then(close) ).then(close)
.catch(() => { .catch(() => {
close({ success: false, message: 'connection closed unexpectedly' }) close({ success: false, message: 'connection closed unexpectedly' })

View file

@ -2,7 +2,7 @@ import { Mutex } from 'async-mutex'
import { runMutexExclusive } from '~/src/lib/discard' import { runMutexExclusive } from '~/src/lib/discard'
export interface CallMessage { name: string, params?: string[] | null } export interface CallMessage { call: string, params?: string[] | null }
export type ResultMessage = { success: true } | { success: false, message: string } export type ResultMessage = { success: true } | { success: false, message: string }
export interface SignalMessage { signal: string } export interface SignalMessage { signal: string }
function isResultMessage (value: any): value is ResultMessage { function isResultMessage (value: any): value is ResultMessage {
@ -18,8 +18,8 @@ function isResultMessage (value: any): value is ResultMessage {
* Opens a WebSocket connection and calls a server action * Opens a WebSocket connection and calls a server action
* @param endpoint Endpoint to call * @param endpoint Endpoint to call
* @param call Function to call * @param call Function to call
* @param onOpen callback for once the connection is opened. The send function can be used to send additional text to the server. * @param onOpen callback for once the connection is opened. The send function can be used to send additional text to the server. It should include newlines.
* @param onText called when the connection receives some text * @param onText called when the connection receives some text, including newlines.
* @returns a promise that is resolved once the conneciton is closed. Rejected if the connection errors. * @returns a promise that is resolved once the conneciton is closed. Rejected if the connection errors.
*/ */
export default async function callServerAction ( export default async function callServerAction (

View file

@ -150,6 +150,7 @@ func (dis *Distillery) allComponents() []initFunc {
auto[*tokens.Tokens], auto[*tokens.Tokens],
//scopes //scopes
auto[*scopes.Never],
auto[*scopes.UserLoggedIn], auto[*scopes.UserLoggedIn],
auto[*scopes.AdminLoggedIn], auto[*scopes.AdminLoggedIn],
auto[*scopes.ListInstancesScope], auto[*scopes.ListInstancesScope],