webui, dockerimg: store the webui assets in a zip and cp them into the container
diff --git a/loop/webui/esbuild.go b/loop/webui/esbuild.go
index 968127c..ca42afa 100644
--- a/loop/webui/esbuild.go
+++ b/loop/webui/esbuild.go
@@ -5,6 +5,8 @@
 package webui
 
 import (
+	"archive/zip"
+	"bytes"
 	"crypto/sha256"
 	"embed"
 	"encoding/hex"
@@ -41,7 +43,7 @@
 	if err != nil {
 		return "", fmt.Errorf("embedded hash: %w", err)
 	}
-	return hex.EncodeToString(h.Sum(nil)), nil
+	return hex.EncodeToString(h.Sum(nil))[:32], nil
 }
 
 func cleanBuildDir(buildDir string) error {
@@ -53,7 +55,6 @@
 			return fs.SkipDir
 		}
 		osPath := filepath.Join(buildDir, path)
-		fmt.Printf("removing %s\n", osPath)
 		os.RemoveAll(osPath)
 		if d.IsDir() {
 			return fs.SkipDir
@@ -101,27 +102,41 @@
 	return nil
 }
 
+func ZipPath() (string, error) {
+	_, hashZip, err := zipPath()
+	return hashZip, err
+}
+
+func zipPath() (cacheDir, hashZip string, err error) {
+	homeDir, err := os.UserHomeDir()
+	if err != nil {
+		return "", "", err
+	}
+	hash, err := embeddedHash()
+	if err != nil {
+		return "", "", err
+	}
+	cacheDir = filepath.Join(homeDir, ".cache", "sketch", "webui")
+	return cacheDir, filepath.Join(cacheDir, "skui-"+hash+".zip"), nil
+}
+
 // Build unpacks and esbuild's all bundleTs typescript files
 func Build() (fs.FS, error) {
-	homeDir, err := os.UserHomeDir()
+	cacheDir, hashZip, err := zipPath()
 	if err != nil {
 		return nil, err
 	}
-	cacheDir := filepath.Join(homeDir, ".cache", "sketch", "webui")
 	buildDir := filepath.Join(cacheDir, "build")
 	if err := os.MkdirAll(buildDir, 0o777); err != nil { // make sure .cache/sketch/build exists
 		return nil, err
 	}
-	hash, err := embeddedHash()
-	if err != nil {
-		return nil, err
-	}
-	finalHashDir := filepath.Join(cacheDir, hash)
-	if _, err := os.Stat(finalHashDir); err == nil {
+	if b, err := os.ReadFile(hashZip); err == nil {
 		// Build already done, serve it out.
-		return os.DirFS(finalHashDir), nil
+		return zip.NewReader(bytes.NewReader(b), int64(len(b)))
 	}
 
+	// TODO: try downloading "https://sketch.dev/webui/"+filepath.Base(hashZip)
+
 	// We need to do a build.
 
 	// Clear everything out of the build directory except node_modules.
@@ -188,38 +203,19 @@
 		return nil, fmt.Errorf("failed to write xterm.css: %w", err)
 	}
 
-	// Everything succeeded, so we move tmpHashDir to finalHashDir
-	if err := os.Rename(tmpHashDir, finalHashDir); err != nil {
+	// Everything succeeded, so we write tmpHashDir to hashZip
+	buf := new(bytes.Buffer)
+	w := zip.NewWriter(buf)
+	if err := w.AddFS(os.DirFS(tmpHashDir)); err != nil {
 		return nil, err
 	}
-	return os.DirFS(finalHashDir), nil
-}
-
-// unpackTS unpacks all the typescript-relevant files from the embedded filesystem into tmpDir.
-func unpackTS(outDir string, embedded fs.FS) error {
-	return fs.WalkDir(embedded, ".", func(path string, d fs.DirEntry, err error) error {
-		if err != nil {
-			return err
-		}
-		tgt := filepath.Join(outDir, path)
-		if d.IsDir() {
-			if err := os.MkdirAll(tgt, 0o777); err != nil {
-				return err
-			}
-			return nil
-		}
-		if strings.HasSuffix(path, ".html") || strings.HasSuffix(path, ".md") || strings.HasSuffix(path, ".css") {
-			return nil
-		}
-		data, err := fs.ReadFile(embedded, path)
-		if err != nil {
-			return err
-		}
-		if err := os.WriteFile(tgt, data, 0o666); err != nil {
-			return err
-		}
-		return nil
-	})
+	if err := w.Close(); err != nil {
+		return nil, err
+	}
+	if err := os.WriteFile(hashZip, buf.Bytes(), 0o666); err != nil {
+		return nil, err
+	}
+	return zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
 }
 
 func esbuildBundle(outDir, src string) error {