remove static folder for relay-server

Hee Sung Son committed Nov 18, 2025 at 16:23 UTC e7f129baa64c6426a76f66e7dbe9f577e919b3da
11 files changed +8 -566
cmd/relay-server/frontend.go
-11
@@ -126,17 +126,6 @@ func createPortalMux() *http.ServeMux {
126
127 mux := http.NewServeMux()
128
129 - // Static file handler for /static/
130 - mux.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
131 - setCORSHeaders(w)
132 - if r.Method == http.MethodOptions {
133 - w.WriteHeader(http.StatusOK)
134 - return
135 - }
136 - path := strings.TrimPrefix(r.URL.Path, "/static/")
137 - servePortalStaticFile(w, r, path)
138 - })
139 -
129 // Static file handler for /frontend/ (for unified caching)
130 mux.HandleFunc("/frontend/", func(w http.ResponseWriter, r *http.Request) {
131 setCORSHeaders(w)
cmd/relay-server/static/admin.js deleted
-30
@@ -1,30 +0,0 @@
1 -function filterCards(q) {
2 - q = (q || "").toLowerCase().trim();
3 - const cards = document.querySelectorAll(".grid .card");
4 - cards.forEach((card) => {
5 - const name = (card.getAttribute("data-name") || "").toLowerCase();
6 - const show = !q || name.includes(q);
7 - card.style.display = show ? "" : "none";
8 - });
9 -}
10 -
11 -function setupSearch() {
12 - const input = document.getElementById("search");
13 - if (!input) return;
14 - input.addEventListener("input", (event) => {
15 - filterCards(event.target.value);
16 - });
17 -}
18 -
19 -(function () {
20 - setupSearch();
21 - const opts = { dateStyle: "medium", timeStyle: "short" };
22 - document.querySelectorAll("time.lastseen").forEach((el) => {
23 - const iso = el.getAttribute("datetime");
24 - if (!iso) return;
25 - const d = new Date(iso);
26 - if (isNaN(d.getTime())) return;
27 - el.textContent = new Intl.DateTimeFormat(undefined, opts).format(d);
28 - el.title = d.toString();
29 - });
30 -})();
cmd/relay-server/static/favicon/apple-touch-icon.png
Binary files a/cmd/relay-server/static/favicon/apple-touch-icon.png and /dev/null differ
cmd/relay-server/static/favicon/favicon-96x96.png
Binary files a/cmd/relay-server/static/favicon/favicon-96x96.png and /dev/null differ
cmd/relay-server/static/favicon/favicon.ico
Binary files a/cmd/relay-server/static/favicon/favicon.ico and /dev/null differ
cmd/relay-server/static/favicon/favicon.svg deleted
-3
@@ -1,3 +0,0 @@
1 -<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1554" height="2500"><svg xmlns="http://www.w3.org/2000/svg" width="1554" height="2500" viewBox="0 0 906.26 1457.543"><path fill="#17C0E9" d="M254.854 137.158c-34.46 84.407-88.363 149.39-110.934 245.675 90.926-187.569 308.397-483.654 554.729-348.685 135.487 74.216 194.878 270.78 206.058 467.566 21.924 385.996-190.977 853.604-467.585 943.057-174.879 56.543-307.375-86.447-364.527-198.115-176.498-344.82 2.041-910.077 182.259-1109.498zm198.13 7.918C202.61 280.257 4.622 968.542 207.322 1270.414c51.713 77.029 194.535 160.648 285.294 71.318-209.061 31.529-288.389-176.143-301.145-340.765 31.411 147.743 139.396 326.12 309.075 253.588 251.957-107.723 376.778-648.46 269.433-966.817 22.394 134.616 15.572 317.711-47.551 412.087 86.655-230.615 7.903-704.478-269.444-554.749z"></path></svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
2 -@media (prefers-color-scheme: dark) { :root { filter: none; } }
3 -</style></svg>
\ No newline at end of file
cmd/relay-server/static/favicon/web-app-manifest-192x192.png
Binary files a/cmd/relay-server/static/favicon/web-app-manifest-192x192.png and /dev/null differ
cmd/relay-server/static/favicon/web-app-manifest-512x512.png
Binary files a/cmd/relay-server/static/favicon/web-app-manifest-512x512.png and /dev/null differ
cmd/relay-server/static/index.html deleted
-136
@@ -1,136 +0,0 @@
1 -<!DOCTYPE html>
2 -<html lang="ko">
3 - <head>
4 - <meta charset="utf-8" />
5 - <meta name="viewport" content="width=device-width, initial-scale=1" />
6 - <title>Portal</title>
7 - <link rel="icon" type="image/x-icon" href="/favicon.ico" />
8 - <link rel="stylesheet" href="/static/style.css" />
9 - </head>
10 -
11 - <body>
12 - <div class="wrap">
13 - <header>
14 - <div class="brand">Portal</div>
15 - <div class="rightbar">
16 - <div class="searchbar" role="search">
17 - <svg
18 - class="icon"
19 - xmlns="http://www.w3.org/2000/svg"
20 - viewBox="0 0 24 24"
21 - fill="currentColor"
22 - aria-hidden="true"
23 - >
24 - <path
25 - d="M15.5 14h-.79l-.28-.27a6.471 6.471 0 0 0 1.57-4.23C15.99 6.01 13.48 3.5 10.49 3.5S5 6.01 5 9s2.51 5.5 5.5 5.5c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l4.25 4.25c.41.41 1.08.41 1.49 0 .41-.41.41-1.08 0-1.49L15.5 14Zm-5 0C8.01 14 6 11.99 6 9.5S8.01 5 10.5 5 15 7.01 15 9.5 12.99 14 10.5 14Z"
26 - />
27 - </svg>
28 - <input
29 - id="search"
30 - type="text"
31 - placeholder="Search by name"
32 - aria-label="Search by name"
33 - />
34 - </div>
35 - <a
36 - class="gh-btn"
37 - href="https://github.com/gosuda/portal"
38 - target="_blank"
39 - rel="noopener"
40 - aria-label="GitHub repository"
41 - title="gosuda/portal"
42 - >
43 - <svg
44 - xmlns="http://www.w3.org/2000/svg"
45 - width="24"
46 - height="24"
47 - viewBox="0 0 24 24"
48 - fill="currentColor"
49 - aria-hidden="true"
50 - >
51 - <path
52 - d="M12 .5C5.73.5.5 5.73.5 12c0 5.08 3.29 9.37 7.86 10.88.58.1.79-.25.79-.56 0-.27-.01-1.16-.02-2.11-3.2.69-3.88-1.39-3.88-1.39-.53-1.35-1.29-1.71-1.29-1.71-1.05-.72.08-.7.08-.7 1.16.08 1.78 1.19 1.78 1.19 1.03 1.77 2.7 1.26 3.36.96.1-.75.4-1.26.73-1.55-2.56-.29-5.26-1.28-5.26-5.72 0-1.26.45-2.3 1.19-3.11-.12-.29-.52-1.45.11-3.02 0 0 .98-.31 3.2 1.19.93-.26 1.94-.39 2.94-.39s2.01.13 2.95.39c2.22-1.5 3.2-1.19 3.2-1.19.63 1.57.23 2.73.12 3.02.74.81 1.19 1.85 1.19 3.11 0 4.45-2.7 5.43-5.28 5.72.41.36.77 1.07.77 2.16 0 1.56-.01 2.81-.01 3.19 0 .31.21.67.8.56C20.22 21.36 23.5 17.08 23.5 12 23.5 5.73 18.27.5 12 .5Z"
53 - />
54 - </svg>
55 - </a>
56 - <div
57 - class="counter"
58 - aria-label="Active devices"
59 - title="Active devices"
60 - >
61 - <svg
62 - class="icon"
63 - xmlns="http://www.w3.org/2000/svg"
64 - viewBox="0 0 24 24"
65 - fill="currentColor"
66 - aria-hidden="true"
67 - >
68 - <circle cx="12" cy="12" r="6" />
69 - </svg>
70 - <span>{{len .Rows}}</span>
71 - </div>
72 - </div>
73 - </header>
74 - <main>
75 - <div class="grid">
76 - {{range .Rows}}
77 - <a
78 - class="card {{if .Connected}}connected{{else}}disconnected{{end}}"
79 - id="peer-{{.Peer}}"
80 - data-peer="{{.Peer}}"
81 - data-name="{{.Name}}"
82 - href="{{.Link}}"
83 - title="{{if .Name}}{{.Name}}{{else}}(unnamed){{end}} 열기"
84 - >
85 - <div class="card-head">
86 - <span
87 - class="status-dot {{if .Connected}}ok{{else}}{{if .StaleRed}}bad{{end}}{{end}}"
88 - ></span>
89 - <div class="title">
90 - {{if .Name}}{{.Name}}{{else}}(unnamed){{end}}
91 - </div>
92 - <svg
93 - class="chevron"
94 - xmlns="http://www.w3.org/2000/svg"
95 - viewBox="0 0 24 24"
96 - fill="none"
97 - stroke="currentColor"
98 - stroke-width="2"
99 - stroke-linecap="round"
100 - stroke-linejoin="round"
101 - aria-hidden="true"
102 - >
103 - <path d="M9 6l6 6-6 6" />
104 - </svg>
105 - </div>
106 - <div class="muted">
107 - Last seen
108 - <time class="lastseen" datetime="{{.LastSeenISO}}"
109 - >{{.LastSeenISO}}</time
110 - ><br />Active ~{{.LastSeen}} ago
111 - </div>
112 - <div class="muted idtext" style="margin-top: auto">
113 - <div>
114 - <strong>Lease ID:</strong>
115 - <span class="mono">{{.Peer}}</span>
116 - </div>
117 - {{if .Description}}<div><strong>Description:</strong> {{.Description}}</div>{{end}}
118 - {{if .Tags}}<div><strong>Tags:</strong> {{.Tags}}</div>{{end}}
119 - {{if .Owner}}<div><strong>Owner:</strong> {{.Owner}}</div>{{end}}
120 - </div>
121 - </a>
122 - {{else}}
123 - <article class="card">
124 - <div class="title">No clients discovered</div>
125 - <div class="muted">
126 - Start a client and ensure bootstrap URLs point at this server's
127 - /relay WebSocket endpoint.
128 - </div>
129 - </article>
130 - {{end}}
131 - </div>
132 - </main>
133 - </div>
134 - <script src="/static/admin.js"></script>
135 - </body>
136 -</html>
cmd/relay-server/static/style.css deleted
-378
@@ -1,378 +0,0 @@
1 -* {
2 - box-sizing: border-box
3 -}
4 -
5 -:root {
6 - --bg: #fafbff;
7 - --panel: #ffffff;
8 - --ink: #0f172a;
9 - --muted: #6b7280;
10 - --line: #e9eef5;
11 - --primary: #2563eb;
12 - --ok: #059669;
13 - --bad: #b91c1c;
14 - --ok-bg: #ecfdf5;
15 - --bad-bg: #fee2e2;
16 -
17 - --radius: 14px;
18 - --radius-pill: 999px;
19 - --shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
20 - --shadow-hover: 0 6px 20px rgba(15, 23, 42, 0.12);
21 - --transition: .2s ease;
22 - --gap: 16px;
23 - --pad: 16px;
24 - --header-pad: 20px 24px;
25 - --wrap-max: 980px;
26 - --fs-base: 16px;
27 - --fs-muted: 13px;
28 - --font: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
29 -}
30 -
31 -body {
32 - margin: 0;
33 - background: var(--bg);
34 - color: var(--ink);
35 - font-family: var(--font);
36 - font-size: var(--fs-base);
37 - line-height: 1.6
38 -}
39 -
40 -.wrap {
41 - max-width: var(--wrap-max);
42 - margin: 0 auto;
43 - padding: calc(var(--pad) * 2) 20px
44 -}
45 -
46 -header {
47 - display: flex;
48 - align-items: center;
49 - gap: var(--gap);
50 - justify-content: space-between;
51 - padding: var(--header-pad);
52 - background: var(--panel);
53 - border: 1px solid var(--line);
54 - border-radius: var(--radius)
55 -}
56 -
57 -.brand {
58 - display: flex;
59 - align-items: center;
60 - height: 36px;
61 - font-weight: 800;
62 - font-size: 24px;
63 - letter-spacing: .2px
64 -}
65 -
66 -.status {
67 - color: var(--ok);
68 - font-weight: 700
69 -}
70 -
71 -.app-left {
72 - display: flex;
73 - align-items: center;
74 - gap: 12px
75 -}
76 -
77 -main {
78 - margin-top: 22px
79 -}
80 -
81 -.section {
82 - background: var(--panel);
83 - border: 1px solid var(--line);
84 - border-radius: var(--radius);
85 - padding: 18px;
86 - margin-bottom: 14px
87 -}
88 -
89 -.grid {
90 - display: grid;
91 - grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
92 - gap: var(--gap);
93 - margin-top: 14px
94 -}
95 -
96 -.card {
97 - background: var(--panel);
98 - border: 1px solid var(--line);
99 - border-radius: var(--radius);
100 - padding: var(--pad);
101 - display: flex;
102 - flex-direction: column;
103 - box-shadow: var(--shadow);
104 - transition: box-shadow var(--transition), transform var(--transition);
105 - text-decoration: none;
106 - color: inherit;
107 - cursor: pointer;
108 - min-height: 180px
109 -}
110 -
111 -.card:hover {
112 - box-shadow: var(--shadow-hover);
113 - transform: translateY(-2px)
114 -}
115 -
116 -.card-head {
117 - display: flex;
118 - align-items: center;
119 - gap: 10px;
120 - margin-bottom: 8px
121 -}
122 -
123 -.chevron {
124 - margin-left: auto;
125 - width: 16px;
126 - height: 16px;
127 - color: var(--muted);
128 - opacity: .9;
129 - transition: transform var(--transition), color var(--transition), opacity var(--transition)
130 -}
131 -
132 -.card:hover .chevron {
133 - transform: translateX(2px);
134 - color: var(--primary);
135 - opacity: 1
136 -}
137 -
138 -.status-dot {
139 - width: 10px;
140 - height: 10px;
141 - border-radius: var(--radius-pill);
142 - background: var(--muted)
143 -}
144 -
145 -.status-dot.ok {
146 - background: var(--ok)
147 -}
148 -
149 -.status-dot.bad {
150 - background: var(--bad)
151 -}
152 -
153 -.title {
154 - font-weight: 800;
155 - margin: 0;
156 - font-size: 18px
157 -}
158 -
159 -.muted {
160 - color: var(--muted);
161 - font-size: var(--fs-muted)
162 -}
163 -
164 -.mono {
165 - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
166 - font-size: var(--fs-muted);
167 - color: #374151;
168 - word-break: break-all
169 -}
170 -
171 -.pill {
172 - display: inline-flex;
173 - align-items: center;
174 - gap: 8px;
175 - padding: 6px 10px;
176 - border-radius: var(--radius-pill);
177 - font-weight: 800;
178 - font-size: var(--fs-muted)
179 -}
180 -
181 -.pill.ok {
182 - background: var(--ok-bg);
183 - color: var(--ok)
184 -}
185 -
186 -.pill.bad {
187 - background: var(--bad-bg);
188 - color: var(--bad)
189 -}
190 -
191 -.pill .dot {
192 - width: 8px;
193 - height: 8px;
194 - border-radius: var(--radius-pill);
195 - background: var(--ok);
196 - display: inline-block
197 -}
198 -
199 -.pill.bad .dot {
200 - background: var(--bad)
201 -}
202 -
203 -.head {
204 - display: flex;
205 - align-items: center;
206 - justify-content: space-between;
207 - gap: 12px
208 -}
209 -
210 -.btn {
211 - display: inline-block;
212 - background: var(--primary);
213 - color: #fff;
214 - text-decoration: none;
215 - border-radius: 8px;
216 - padding: 6px 10px;
217 - font-weight: 700;
218 - font-size: var(--fs-muted);
219 - line-height: 1;
220 - margin-top: 6px;
221 - transition: filter var(--transition)
222 -}
223 -
224 -.btn:hover {
225 - filter: brightness(0.95)
226 -}
227 -
228 -.btn.outline {
229 - background: transparent;
230 - color: var(--primary);
231 - border: 1px solid var(--primary)
232 -}
233 -
234 -.actions {
235 - margin-top: auto;
236 - display: flex;
237 - justify-content: flex-end
238 -}
239 -
240 -.card:focus-visible {
241 - outline: 2px solid var(--primary);
242 - outline-offset: 2px
243 -}
244 -
245 -.searchbar {
246 - flex: 1 1 320px;
247 - width: 320px;
248 - min-width: 80px;
249 - max-width: 50vw;
250 - margin: 0;
251 - display: flex;
252 - align-items: center;
253 - gap: 10px;
254 - height: 36px;
255 - padding: 0 12px;
256 - border: 1px solid var(--line);
257 - border-radius: var(--radius-pill);
258 - background: #fff
259 -}
260 -
261 -.searchbar:focus-within {
262 - box-shadow: 0 1px 6px rgba(32, 33, 36, .28);
263 - border-color: #dfe1e5
264 -}
265 -
266 -.searchbar input {
267 - width: 100%;
268 - min-width: 0;
269 - height: 100%;
270 - border: none;
271 - outline: none;
272 - font-size: 14px;
273 - background: transparent;
274 - color: #111
275 -}
276 -
277 -.searchbar .icon {
278 - width: 18px;
279 - height: 18px;
280 - color: #9aa0a6
281 -}
282 -
283 -.topbar {
284 - display: flex;
285 - align-items: center;
286 - gap: var(--gap);
287 - justify-content: space-between
288 -}
289 -
290 -.rightbar {
291 - display: flex;
292 - align-items: center;
293 - gap: 12px;
294 - margin-left: auto;
295 - min-width: 0
296 -}
297 -
298 -.gh-btn {
299 - width: 36px;
300 - height: 36px;
301 - border-radius: var(--radius-pill);
302 - display: inline-flex;
303 - align-items: center;
304 - justify-content: center;
305 - color: #000;
306 - background: #fff;
307 - border: 1px solid var(--line);
308 - transition: box-shadow var(--transition), transform var(--transition)
309 -}
310 -
311 -.gh-btn:hover {
312 - box-shadow: var(--shadow)
313 -}
314 -
315 -.counter {
316 - display: inline-flex;
317 - align-items: center;
318 - gap: 6px;
319 - height: 36px;
320 - padding: 0 10px;
321 - border-radius: var(--radius-pill);
322 - background: var(--ok-bg);
323 - color: var(--ok);
324 - font-weight: 800
325 -}
326 -
327 -.counter .icon {
328 - width: 18px;
329 - height: 18px
330 -}
331 -
332 -/* Wrap long IDs at card bottom */
333 -.idtext {
334 - word-break: break-all;
335 - overflow-wrap: anywhere
336 -}
337 -
338 -@media (max-width: 640px) {
339 - .wrap {
340 - padding: 20px 14px
341 - }
342 -
343 - header {
344 - flex-wrap: nowrap;
345 - gap: 8px;
346 - padding: 16px
347 - }
348 -
349 - .brand {
350 - white-space: nowrap
351 - }
352 -
353 - .rightbar {
354 - margin-left: auto;
355 - width: auto;
356 - flex-wrap: nowrap;
357 - gap: 8px;
358 - min-width: 0
359 - }
360 -
361 - .searchbar {
362 - flex: 1 1 auto;
363 - width: auto;
364 - min-width: 60px;
365 - max-width: none
366 - }
367 -
368 - .gh-btn {
369 - width: 32px;
370 - height: 32px
371 - }
372 -
373 - .counter {
374 - height: 32px;
375 - padding: 0 8px;
376 - font-size: 12px
377 - }
378 -}
\ No newline at end of file
cmd/relay-server/view.go
+8 -8
@@ -2,7 +2,6 @@ package main
2
3 import (
4 "context"
5 - "embed"
5 "encoding/json"
6 "fmt"
7 "net/http"
@@ -12,17 +11,18 @@ import (
11 "github.com/gorilla/websocket"
12 "github.com/rs/zerolog/log"
13
14 + pathpkg "path"
15 +
16 "gosuda.org/portal/portal"
17 "gosuda.org/portal/portal/utils/wsstream"
18 "gosuda.org/portal/sdk"
19 )
20
20 -//go:embed static
21 -var assetsFS embed.FS
22 -
21 func serveAsset(mux *http.ServeMux, route, assetPath, contentType string) {
22 mux.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
25 - b, err := assetsFS.ReadFile(assetPath)
23 +
24 + fullPath := pathpkg.Join("app", assetPath)
25 + b, err := appFS.ReadFile(fullPath)
26 if err != nil {
27 http.NotFound(w, r)
28 return
@@ -45,9 +45,9 @@ func serveHTTP(_ context.Context, addr string, serv *portal.RelayServer, nodeID
45 appMux := http.NewServeMux()
46
47 // Serve embedded favicons (ico/png/svg) for portal UI
48 - serveAsset(appMux, "/favicon.ico", "static/favicon/favicon.ico", "image/x-icon")
49 - serveAsset(appMux, "/favicon.png", "static/favicon/favicon.png", "image/png")
50 - serveAsset(appMux, "/favicon.svg", "static/favicon/favicon.svg", "image/svg+xml")
48 + serveAsset(appMux, "/favicon.ico", "/favicon.ico", "image/x-icon")
49 + serveAsset(appMux, "/favicon.png", "/favicon.png", "image/png")
50 + serveAsset(appMux, "/favicon.svg", "/favicon.svg", "image/svg+xml")
51
52 // Portal app assets (JS, CSS, etc.) - served from /app/
53 appMux.HandleFunc("/app/", func(w http.ResponseWriter, r *http.Request) {