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) {