diff --git a/Makefile b/Makefile
index 774795d..1ff4a2d 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,10 @@
# Path parameters
GO ?= $(shell which go 2>/dev/null)
DOCKER ?= $(shell which docker 2>/dev/null)
+WASMBUILD ?= $(shell which wasmbuild 2>/dev/null)
BUILDDIR ?= build
CMDDIR=$(wildcard cmd/*)
+WASMDIR=$(wildcard wasm/*)
# Set OS and Architecture
ARCH ?= $(shell arch | tr A-Z a-z | sed 's/x86_64/amd64/' | sed 's/i386/amd64/' | sed 's/armv7l/arm/' | sed 's/aarch64/arm64/')
@@ -16,7 +18,7 @@ BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitTag=$(shell git describe --t
BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitBranch=$(shell git name-rev HEAD --name-only --always)
BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitHash=$(shell git rev-parse HEAD)
BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GoBuildTime=$(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
-BUILD_FLAGS = -ldflags "-s -w ${BUILD_LD_FLAGS}"
+BUILD_FLAGS = -ldflags="-s -w ${BUILD_LD_FLAGS}"
# Docker
DOCKER_REPO ?= ghcr.io/mutablelogic/pgmanager
@@ -29,8 +31,22 @@ all: tidy $(CMDDIR)
# Rules for building
.PHONY: $(CMDDIR)
$(CMDDIR): go-dep mkdir
- @echo 'building $@'
- @$(GO) build $(BUILD_FLAGS) -o ${BUILDDIR}/$(shell basename $@) ./$@
+ @echo 'go build $@'
+ @rm -rf ${BUILDDIR}/$(shell basename $@)
+ @$(GO) build -tags frontend $(BUILD_FLAGS) -o ${BUILDDIR}/$(shell basename $@) ./$@
+
+# Rules for building
+.PHONY: $(WASMDIR)
+$(WASMDIR): go-dep wasmbuild-dep mkdir
+ @echo 'wasmbuild $@'
+ @$(GO) get github.com/djthorpe/go-wasmbuild/pkg/bootstrap github.com/djthorpe/go-wasmbuild/pkg/bootstrap/extra github.com/djthorpe/go-wasmbuild/pkg/mvc
+ @${WASMBUILD} build --go-flags='$(BUILD_FLAGS)' -o ${BUILDDIR}/wasm/$(shell basename $@) ./$@ && \
+ mv ${BUILDDIR}/wasm/$(shell basename $@)/wasm_exec.html ${BUILDDIR}/wasm/$(shell basename $@)/index.html && \
+ cp etc/embed.go ${BUILDDIR}/wasm/$(shell basename $@)/
+
+# Build pgmanager with embedded frontend
+.PHONY: pgmanager
+pgmanager: wasm/pgmanager cmd/pgmanager
# Build the docker image
.PHONY: docker
@@ -68,8 +84,11 @@ mkdir:
@install -d $(BUILDDIR)
.PHONY: go-dep tidy
-tidy:
+tidy: mkdir
@echo 'go tidy'
+ @install -d ${BUILDDIR}/wasm/pgmanager
+ @cp -n etc/embed.go ${BUILDDIR}/wasm/pgmanager/ 2>/dev/null || true
+ @echo 'module github.com/mutablelogic/go-pg/build/wasm/pgmanager' > ${BUILDDIR}/wasm/pgmanager/go.mod
@$(GO) mod tidy
.PHONY: clean
@@ -88,3 +107,7 @@ go-dep:
.PHONY: docker-dep
docker-dep:
@test -f "${DOCKER}" && test -x "${DOCKER}" || (echo "Missing docker binary" && exit 1)
+
+.PHONY: wasmbuild-dep
+wasmbuild-dep:
+ @test -f "${WASMBUILD}" && test -x "${WASMBUILD}" || (echo "Missing wasmbuild binary" && exit 1)
\ No newline at end of file
diff --git a/cmd/pgmanager/server.go b/cmd/pgmanager/server.go
index cbfa38b..cb794d4 100644
--- a/cmd/pgmanager/server.go
+++ b/cmd/pgmanager/server.go
@@ -10,7 +10,6 @@ import (
"github.com/mutablelogic/go-pg"
"github.com/mutablelogic/go-pg/pkg/manager"
"github.com/mutablelogic/go-pg/pkg/manager/httphandler"
- "github.com/mutablelogic/go-server/pkg/httpresponse"
"github.com/mutablelogic/go-server/pkg/httpserver"
)
@@ -75,12 +74,8 @@ func (cmd *RunServer) Run(ctx *Globals) error {
// Register HTTP handlers
router := http.NewServeMux()
- httphandler.RegisterHandlers(router, ctx.HTTP.Prefix, manager)
-
- // Catch all handler returns a "not found" error
- router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- _ = httpresponse.Error(w, httpresponse.ErrNotFound, r.URL.String())
- })
+ httphandler.RegisterBackendHandlers(router, ctx.HTTP.Prefix, manager)
+ httphandler.RegisterFrontendHandler(router, "")
// Create a TLS config
var tlsconfig *tls.Config
diff --git a/etc/Dockerfile b/etc/Dockerfile
index 39dea6e..d73253c 100644
--- a/etc/Dockerfile
+++ b/etc/Dockerfile
@@ -7,7 +7,8 @@ ARG ARCH=amd64
FROM --platform=${OS}/${ARCH} golang:1.24 AS builder
WORKDIR /usr/src/app
COPY . .
-RUN OS=${OS} ARCH=${ARCH} make cmd/pgmanager
+RUN go install github.com/djthorpe/go-wasmbuild/cmd/wasmbuild@latest && \
+ OS=${OS} ARCH=${ARCH} make pgmanager
# Runtime stage
FROM --platform=${OS}/${ARCH} debian:bookworm-slim
@@ -21,5 +22,5 @@ RUN apt-get update && \
LABEL org.opencontainers.image.source=${SOURCE}
# Entrypoint when running the server
-EXPOSE 80 443
+EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/pgmanager"]
diff --git a/etc/embed.go b/etc/embed.go
new file mode 100644
index 0000000..eac6e72
--- /dev/null
+++ b/etc/embed.go
@@ -0,0 +1,6 @@
+package frontend
+
+import "embed"
+
+//go:embed *.html *.js *.wasm *.png
+var FS embed.FS
diff --git a/go.mod b/go.mod
index 52db797..3007ce2 100644
--- a/go.mod
+++ b/go.mod
@@ -6,9 +6,11 @@ toolchain go1.24.2
require (
github.com/alecthomas/kong v1.13.0
+ github.com/djthorpe/go-wasmbuild v0.0.1
github.com/docker/go-connections v0.6.0
github.com/jackc/pgx/v5 v5.7.6
github.com/mutablelogic/go-client v1.2.2
+ github.com/mutablelogic/go-pg/build/wasm/pgmanager v0.0.0-00010101000000-000000000000
github.com/mutablelogic/go-server v1.5.17
github.com/prometheus/client_golang v1.23.2
github.com/stretchr/testify v1.11.1
@@ -42,6 +44,7 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
@@ -57,6 +60,7 @@ require (
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/mutablelogic/go-tokenizer v0.0.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
@@ -74,18 +78,23 @@ require (
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
- go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.46.0 // indirect
+ golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
+ google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
// Redirect old module path to this module (for transitive dependencies)
replace github.com/djthorpe/go-pg => ./
+
+// Frontend embedded files (local path, built by make wasm/pgmanager)
+replace github.com/mutablelogic/go-pg/build/wasm/pgmanager => ./build/wasm/pgmanager
diff --git a/go.sum b/go.sum
index d1d72c0..eb8e566 100644
--- a/go.sum
+++ b/go.sum
@@ -41,6 +41,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djthorpe/go-errors v1.0.3 h1:GZeMPkC1mx2vteXLI/gvxZS0Ee9zxzwD1mcYyKU5jD0=
github.com/djthorpe/go-errors v1.0.3/go.mod h1:HtfrZnMd6HsX75Mtbv9Qcnn0BqOrrFArvCaj3RMnZhY=
+github.com/djthorpe/go-wasmbuild v0.0.1 h1:HoPNhNcrK5S0ze2TS46NouoAyXPqupgQImNfwaM1b0o=
+github.com/djthorpe/go-wasmbuild v0.0.1/go.mod h1:T3vqsVbmzws0VG50oXY5OiQMDIdBQsgYRjkoVIedPFM=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
@@ -130,6 +132,8 @@ github.com/mutablelogic/go-client v1.2.2 h1:ZKIQFL4qYydyyBb2s3BaMUwIF3dlA5HCOTDN
github.com/mutablelogic/go-client v1.2.2/go.mod h1:Y31sWrM22cSWiiOtvkc0MHSpFt5hMsk/CsjJ0d8iB10=
github.com/mutablelogic/go-server v1.5.17 h1:NXuP8IWIM3DLlu/I375fK8pq+OIYHil3Xun1GGklUWM=
github.com/mutablelogic/go-server v1.5.17/go.mod h1:HCX8WZtE3RXR4i+npBvCdILfnBelomDIe8/B68O/MA4=
+github.com/mutablelogic/go-tokenizer v0.0.1 h1:3gSF+zoMq4tLpH+XYawkVUb7qmWuYNCez1CTs/iX3mU=
+github.com/mutablelogic/go-tokenizer v0.0.1/go.mod h1:zdAyIhfqUKxFXb8MwChbXNwMOZt/5NlUylmx6Qjr4v8=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
diff --git a/pkg/manager/httphandler/doc.go b/pkg/manager/httphandler/doc.go
index 2d4364c..2299798 100644
--- a/pkg/manager/httphandler/doc.go
+++ b/pkg/manager/httphandler/doc.go
@@ -3,7 +3,7 @@
//
// Register all handlers with an http.ServeMux:
//
-// httphandler.RegisterHandlers(mux, "/api/v1", mgr)
+// httphandler.RegisterBackendHandlers(mux, "/api/v1", mgr)
//
// This registers endpoints for roles, databases, schemas, objects, tablespaces,
// extensions, connections, settings, statements, replication slots, and
diff --git a/pkg/manager/httphandler/frontend.go b/pkg/manager/httphandler/frontend.go
new file mode 100644
index 0000000..346ba84
--- /dev/null
+++ b/pkg/manager/httphandler/frontend.go
@@ -0,0 +1,17 @@
+//go:build frontend
+
+package httphandler
+
+import (
+ "net/http"
+
+ // Packages
+ frontend "github.com/mutablelogic/go-pg/build/wasm/pgmanager"
+)
+
+// RegisterFrontendHandler registers the frontend static file handler
+func RegisterFrontendHandler(router *http.ServeMux, prefix string) {
+ // Serve static files
+ fileServer := http.FileServer(http.FS(frontend.FS))
+ router.Handle(joinPath(prefix, "/"), http.StripPrefix(prefix, fileServer))
+}
diff --git a/pkg/manager/httphandler/frontend_excluded.go b/pkg/manager/httphandler/frontend_excluded.go
new file mode 100644
index 0000000..3a4230f
--- /dev/null
+++ b/pkg/manager/httphandler/frontend_excluded.go
@@ -0,0 +1,18 @@
+//go:build !frontend
+
+package httphandler
+
+import (
+ "net/http"
+
+ // Packages
+ "github.com/mutablelogic/go-server/pkg/httpresponse"
+)
+
+// RegisterFrontendHandler registers a fallback handler when frontend is not included
+func RegisterFrontendHandler(router *http.ServeMux, prefix string) {
+ // Catch all handler returns a "not found" error
+ router.HandleFunc(joinPath(prefix, "/"), func(w http.ResponseWriter, r *http.Request) {
+ _ = httpresponse.Error(w, httpresponse.ErrNotFound, r.URL.String())
+ })
+}
diff --git a/pkg/manager/httphandler/httphandler.go b/pkg/manager/httphandler/httphandler.go
index 2746a10..74d8841 100644
--- a/pkg/manager/httphandler/httphandler.go
+++ b/pkg/manager/httphandler/httphandler.go
@@ -14,7 +14,7 @@ import (
///////////////////////////////////////////////////////////////////////////////
// PUBLIC METHODS
-func RegisterHandlers(router *http.ServeMux, prefix string, manager *manager.Manager) {
+func RegisterBackendHandlers(router *http.ServeMux, prefix string, manager *manager.Manager) {
RegisterConnectionHandlers(router, prefix, manager)
RegisterDatabaseHandlers(router, prefix, manager)
RegisterExtensionHandlers(router, prefix, manager)
diff --git a/wasm/pgmanager/main.go b/wasm/pgmanager/main.go
new file mode 100644
index 0000000..b546fa0
--- /dev/null
+++ b/wasm/pgmanager/main.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+ // Packages
+ bs "github.com/djthorpe/go-wasmbuild/pkg/bootstrap"
+ bsextra "github.com/djthorpe/go-wasmbuild/pkg/bootstrap/extra"
+ mvc "github.com/djthorpe/go-wasmbuild/pkg/mvc"
+)
+
+func main() {
+ // Navigation controller
+ controller := bsextra.NavbarController(navbar())
+
+ // Run the application
+ mvc.New(controller.Views()[0]).Run()
+}
+
+func navbar() mvc.View {
+ return bs.NavBar("main",
+ bs.WithPosition(bs.Sticky|bs.Top), bs.WithTheme(bs.Dark), bs.WithSize(bs.Medium),
+ bs.NavItem("#roles", "Roles"),
+ ).Label(
+ bs.Icon("bootstrap-fill", mvc.WithClass("me-2")), "pgmanager",
+ )
+}
diff --git a/wasm/pgmanager/wasmbuild.yaml b/wasm/pgmanager/wasmbuild.yaml
new file mode 100644
index 0000000..eb92fc4
--- /dev/null
+++ b/wasm/pgmanager/wasmbuild.yaml
@@ -0,0 +1,9 @@
+vars:
+ Title: "pgmanager"
+ Header: |
+
+
+
+
+
+