change default portal domain to localhost for local development

Kim committed Nov 6, 2025 at 18:46 UTC a04eab5d1b649cd3ff7d69b20052daa837107026
7 files changed +63 -24
Dockerfile
+3 -2
@@ -14,7 +14,8 @@ RUN go mod download
14 COPY . .
15
16 # Build WASM and server
17 -RUN make build-wasm
17 +ARG BOOTSTRAPS=""
18 +RUN make build-wasm BOOTSTRAPS="$BOOTSTRAPS"
19
20 # Build server
21 RUN make build-server
@@ -29,7 +30,7 @@ COPY --from=builder /src/dist /app/dist
30
31 # Set default environment variables
32 ENV STATIC_DIR=/app/dist
32 -ENV PORTAL_DOMAIN=portal.gosuda.org
33 +ENV PORTAL_DOMAIN=localhost
34
35 # Expose ports
36 # 4017: relay server and portal frontend
Makefile
+16 -7
@@ -1,9 +1,12 @@
1 SHELL := /bin/sh
2
3 -.PHONY: build build-wasm build-server clean
3 +.PHONY: run build build-wasm build-server clean
4 +
5 +run:
6 + ./bin/relay-server
7
8 # Convenience target: build wasm then server
6 -build: build-protoc build-wasm build-server
9 +build: build-protoc build-wasm build-server build-tunnel
10
11 build-protoc:
12 protoc -I . \
@@ -14,12 +17,20 @@ build-protoc:
17 portal/core/proto/rdsec/rdsec.proto \
18 portal/core/proto/rdverb/rdverb.proto
19
20 +BOOTSTRAPS ?= ""
21 +
22 # Build WASM artifacts with wasm-opt optimization and generate manifest
23 build-wasm:
24 @echo "[wasm] building webclient WASM..."
25 @mkdir -p dist
21 -
22 - GOOS=js GOARCH=wasm go build -trimpath -ldflags "-s -w" -o dist/portal.wasm ./cmd/webclient
26 +
27 + # Prepare optional link flags for bootstrap injection
28 + @WASM_LDFLAGS=""; \
29 + if [ -n "$(BOOTSTRAPS)" ]; then \
30 + WASM_LDFLAGS="-X main.bootstrapServersCSV=$(BOOTSTRAPS)"; \
31 + echo "[wasm] injecting bootstraps: $(BOOTSTRAPS)"; \
32 + fi; \
33 + GOOS=js GOARCH=wasm go build -trimpath -ldflags "-s -w $$WASM_LDFLAGS" -o dist/portal.wasm ./cmd/webclient
34
35 @echo "[wasm] optimizing with wasm-opt..."
36 @if command -v wasm-opt >/dev/null 2>&1; then \
@@ -58,6 +69,4 @@ build-tunnel:
69
70 clean:
71 rm -rf bin
61 - rm -rf dist
62 - rm -rf sdk/wasm
63 - rm -rf portal/wasm/pkg
72 + rm -rf dist
\ No newline at end of file
cmd/relay-server/frontend.go
+16 -4
@@ -20,8 +20,8 @@ import (
20 // staticDir is the directory where static files are located
21 var staticDir = "./dist"
22
23 -// portalDomain is the domain for portal frontend (e.g., "portal.gosuda.org")
24 -var portalDomain = "portal.gosuda.org"
23 +// portalDomain is the domain for portal frontend.
24 +var portalDomain = "localhost"
25
26 // wasmCache stores pre-compressed WASM files in memory
27 type wasmCacheEntry struct {
@@ -419,8 +419,20 @@ func getContentType(ext string) string {
419
420 // isPortalSubdomain checks if the host is a portal subdomain (*.{portalDomain})
421 func isPortalSubdomain(host string) bool {
422 - // Check if it ends with .{portalDomain} or is exactly {portalDomain}
423 - return strings.HasSuffix(host, "."+portalDomain)
422 + if portalDomain == "" {
423 + return false
424 + }
425 + // Trim port from request host
426 + if i := strings.Index(host, ":"); i >= 0 {
427 + host = host[:i]
428 + }
429 + // Allow optional ":port" in portalDomain for local dev
430 + base := portalDomain
431 + if j := strings.Index(base, ":"); j >= 0 {
432 + base = base[:j]
433 + }
434 + // Check if it ends with .{base}
435 + return strings.HasSuffix(host, "."+base)
436 }
437
438 // isHexString checks if a string contains only hexadecimal characters
cmd/relay-server/main.go
+1 -1
@@ -35,7 +35,7 @@ func main() {
35 }
36 defaultPortalDomain := os.Getenv("PORTAL_DOMAIN")
37 if defaultPortalDomain == "" {
38 - defaultPortalDomain = "portal.gosuda.org"
38 + defaultPortalDomain = "localhost"
39 }
40 var flagBootstrapsCSV string
41 flag.StringVar(&flagBootstrapsCSV, "bootstraps", "ws://localhost:4017/relay", "bootstrap addresses (comma-separated)")
cmd/relay-server/static/index.html
+15
@@ -90,6 +90,21 @@
90 });
91 } catch (_) { }
92 })();
93 + (function () {
94 + try {
95 + var port = window.location.port;
96 + if (!port) return; // default ports (80/443)
97 + document.querySelectorAll('a.card[href^="//"]').forEach(function (a) {
98 + try {
99 + var u = new URL(a.href);
100 + if (!u.port) { // only if not already set
101 + u.port = port;
102 + a.href = u.toString();
103 + }
104 + } catch (_) { }
105 + });
106 + } catch (_) { }
107 + })();
108 </script>
109 </body>
110
cmd/webclient/main_js.go
+3 -2
@@ -28,7 +28,7 @@ import (
28 )
29
30 var (
31 - bootstrapServers = []string{"wss://portal.gosuda.org/relay"}
31 + bootstrapServers string = "ws://localhost:4017/relay"
32 rdClient *sdk.RDClient
33 )
34
@@ -538,9 +538,10 @@ func (p *Proxy) handleDisconnect(w http.ResponseWriter, r *http.Request, connID
538 func main() {
539 log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
540 var err error
541 + var bootstrapServerList = strings.Split(bootstrapServers, ",")
542
543 rdClient, err = sdk.NewClient(
543 - sdk.WithBootstrapServers(bootstrapServers),
544 + sdk.WithBootstrapServers(bootstrapServerList),
545 sdk.WithDialer(WebSocketDialerJS()),
546 )
547 if err != nil {
cmd/webclient/service-worker.js
+9 -8
@@ -1,6 +1,7 @@
1 +const BASE_PATH = new URL('./', self.location).pathname;
2 // const wasm_exec_URL = "https://cdn.jsdelivr.net/gh/golang/go@go1.25.3/lib/wasm/wasm_exec.js";
2 -const wasm_exec_URL = "/wasm_exec.js";
3 -const manifest_URL = "/manifest.json";
3 +const wasm_exec_URL = BASE_PATH + "wasm_exec.js";
4 +const manifest_URL = BASE_PATH + "manifest.json";
5
6 importScripts(wasm_exec_URL);
7
@@ -68,8 +69,8 @@ async function runWASM() {
69
70 try {
71 const manifest = await fetchManifest();
71 - // Use content-addressed path: /static/<sha256>.wasm
72 - const wasm_URL = `/static/${manifest.wasmFile}`;
72 + // Use content-addressed path under scope: <BASE_PATH>/static/<sha256>.wasm
73 + const wasm_URL = `${BASE_PATH}static/${manifest.wasmFile}`;
74
75 const go = new Go();
76
@@ -204,7 +205,7 @@ self.addEventListener("fetch", (e) => {
205 }
206
207 // Serve portal.mp4 from cache or fetch from origin
207 - if (url.pathname === "/portal.mp4") {
208 + if (url.pathname === BASE_PATH + "portal.mp4") {
209 console.log("[SW] Fetching portal.mp4");
210 e.respondWith(
211 (async () => {
@@ -212,7 +213,7 @@ self.addEventListener("fetch", (e) => {
213 await fetchManifest();
214 // Try to get from cache first
215 const cache = await caches.open(currentCacheVersion);
215 - const cachedResponse = await cache.match("/portal.mp4");
216 + const cachedResponse = await cache.match(BASE_PATH + "portal.mp4");
217 if (cachedResponse) {
218 console.log("[SW] Found portal.mp4 in cache");
219 return cachedResponse;
@@ -222,7 +223,7 @@ self.addEventListener("fetch", (e) => {
223 const response = await fetch(e.request);
224 if (response.ok) {
225 console.log("[SW] Caching portal.mp4");
225 - cache.put("/portal.mp4", response.clone());
226 + cache.put(BASE_PATH + "portal.mp4", response.clone());
227 }
228 return response;
229 } catch (error) {
@@ -242,7 +243,7 @@ self.addEventListener("fetch", (e) => {
243 // send auto refresh page
244 e.respondWith(
245 new Response(
245 - "<html><head><meta http-equiv='refresh' content='0'></head><body><h1>Sorry, Service Worker failed to process the request. Please refresh the page.</h1></body></html>",
246 + "<html><head><meta http-equiv='refresh' content='0'></head><body>Service Worker failed to process the request. Please refresh the page.</body></html>",
247 { status: 500, headers: { "Content-Type": "text/html" } }
248 )
249 );