relay-server: update view
Kim committed
Oct 29, 2025 at 18:43 UTC
c4f401db9b289cf89be141f9fc65898a6363f106
1 file changed
+72
-39
cmd/relay-server/view.go
+72
-39
@@ -332,65 +332,98 @@ var serverTmpl = template.Must(template.New("admin-index").Parse(`<!doctype html
332
}
333
body { margin:0; background:var(--bg); color:var(--ink); font-family:sans-serif; font-size:16px; line-height:1.6 }
334
.wrap { max-width: 980px; margin: 0 auto; padding: 32px 20px }
335
- header { display:flex; align-items:center; justify-content:space-between; padding: 20px 24px; background:var(--panel); border:1px solid var(--line); border-radius: 14px }
336
- .brand { font-weight:800; font-size:22px; letter-spacing:.2px }
335
+ header { display:flex; align-items:center; gap:16px; justify-content:space-between; padding: 20px 24px; background:var(--panel); border:1px solid var(--line); border-radius: 14px }
336
+ .brand { display:flex; align-items:center; height:36px; font-weight:800; font-size:18px; letter-spacing:.2px }
337
.status { color:var(--ok); font-weight:700 }
338
+ .app-left { display:flex; align-items:center; gap:12px }
339
main { margin-top: 22px }
340
.section { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:18px; margin-bottom:14px }
341
+ .grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap:16px; margin-top:14px }
342
+ .card { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:16px; display:flex; flex-direction:column; box-shadow: 0 1px 2px rgba(15,23,42,0.06); transition: box-shadow .2s ease, transform .2s ease; text-decoration:none; color:inherit; cursor:pointer; min-height: 180px }
343
+ .card:hover { box-shadow: 0 6px 20px rgba(15,23,42,0.12); transform: translateY(-2px) }
344
+ .card-head { display:flex; align-items:center; gap:10px; margin-bottom:8px }
345
+ .status-dot { width:10px; height:10px; border-radius:999px; background:var(--muted) }
346
+ .status-dot.ok { background: var(--ok) }
347
+ .status-dot.bad { background: var(--bad) }
348
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; color:#374151; word-break: break-all }
341
- .title { font-weight:800; margin:0 0 10px 0; font-size:18px }
342
- .muted { color:var(--muted); font-size:14px }
349
+ .title { font-weight:800; margin:0; font-size:16px }
350
+ .muted { color:var(--muted); font-size:13px }
351
.pill { display:inline-flex; align-items:center; gap:8px; padding:6px 10px; border-radius:999px; font-weight:800; font-size:13px }
352
.pill.ok { background:var(--ok-bg); color:var(--ok) }
353
.pill.bad { background:var(--bad-bg); color:var(--bad) }
354
.pill .dot { width:8px; height:8px; border-radius:999px; background:var(--ok); display:inline-block }
355
.pill.bad .dot { background:var(--bad) }
356
.head { display:flex; align-items:center; justify-content:space-between; gap:12px }
349
- .btn { display:inline-block; background:var(--primary); color:#fff; text-decoration:none; border-radius:10px; padding:10px 14px; font-weight:800; margin-top:8px }
357
+ .btn { display:inline-block; background:var(--primary); color:#fff; text-decoration:none; border-radius:8px; padding:6px 10px; font-weight:700; font-size:13px; line-height:1; margin-top:6px }
358
+ .btn.outline { background:transparent; color:var(--primary); border:1px solid var(--primary) }
359
+ .actions { margin-top:auto; display:flex; justify-content:flex-end }
360
+ /* Google-like search bar */
361
+ .searchbar { width: 320px; max-width: 50vw; margin: 0; display:flex; align-items:center; gap:10px; height:36px; padding:0 12px; border:1px solid var(--line); border-radius:999px; background:#fff }
362
+ .searchbar:focus-within { box-shadow: 0 1px 6px rgba(32,33,36,.28); border-color:#dfe1e5 }
363
+ .searchbar input { width:100%; height:100%; border:none; outline:none; font-size:14px; background:transparent; color:#111 }
364
+ .searchbar .icon { width:18px; height:18px; color:#9aa0a6 }
365
+ .topbar { display:flex; align-items:center; gap:16px; justify-content:space-between }
366
+ .rightbar { display:flex; align-items:center; gap:12px; margin-left:auto }
367
+ .gh-btn { width:36px; height:36px; border-radius:999px; display:inline-flex; align-items:center; justify-content:center; color:#000; background:#fff; border:1px solid var(--line) }
368
+ .counter { display:inline-flex; align-items:center; gap:6px; height:36px; padding:0 10px; border-radius:999px; background:var(--ok-bg); color:var(--ok); font-weight:800 }
369
+ .counter .icon { width:18px; height:18px }
370
</style>
371
</head>
372
<body>
373
<div class="wrap">
374
<header>
375
<div class="brand">Portal</div>
356
- <div class="status">Admin</div>
376
+ <div class="rightbar">
377
+ <div class="searchbar" role="search">
378
+ <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
379
+ <path 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"/>
380
+ </svg>
381
+ <input id="search" type="text" placeholder="Search by name" aria-label="Search by name" oninput="filterCards(this.value)">
382
+ </div>
383
+ <a class="gh-btn" href="https://github.com/gosuda/portal" target="_blank" rel="noopener" aria-label="GitHub repository" title="gosuda/portal">
384
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
385
+ <path 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"/>
386
+ </svg>
387
+ </a>
388
+ <div class="counter" aria-label="Active clients" title="Active clients">
389
+ <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
390
+ <path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3 1.34 3 3 3Zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5 5 6.34 5 8s1.34 3 3 3Zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5C18 14.17 13.33 13 11 13Zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h4v-2.5c0-2.33-4.67-3.5-7-3.5Z"/>
391
+ </svg>
392
+ <span>{{len .Rows}}</span>
393
+ </div>
394
+ </div>
395
</header>
396
<main>
359
- <section class="section">
360
- <div class="title">Server</div>
361
- <div class="mono">Server ID: {{.NodeID}}</div>
362
- {{if .Bootstraps}}
363
- <div class="muted" style="margin-top:6px">Bootstrap URLs</div>
364
- <div class="mono">{{range .Bootstraps}}<a href="{{.}}" target="_blank" rel="noreferrer noopener">{{.}}</a><br/>{{end}}</div>
365
- {{end}}
366
- <div class="muted" style="margin-top:6px">Active clients: {{len .Rows}}</div>
367
- </section>
368
- {{range .Rows}}
369
- <section class="section" id="peer-{{.Peer}}" data-peer="{{.Peer}}" data-name="{{.Name}}">
370
- <div class="head">
371
- <div class="title">{{if .Name}}{{.Name}}{{else}}(unnamed){{end}}</div>
372
- <div>
373
- <span class="muted" style="margin-right:8px">{{.Kind}}</span>
374
- {{if .Connected}}
375
- <span class="pill ok"><span class="dot"></span>Connected</span>
376
- {{else}}
377
- <span class="pill bad"><span class="dot"></span>Disconnected</span>
378
- {{end}}
397
+
398
+
399
+ <div class="grid">
400
+ {{range .Rows}}
401
+ <a class="card {{if .Connected}}connected{{else}}disconnected{{end}}" id="peer-{{.Peer}}" data-peer="{{.Peer}}" data-name="{{.Name}}" href="{{.Link}}">
402
+ <div class="card-head">
403
+ <span class="status-dot {{if .Connected}}ok{{else}}bad{{end}}"></span>
404
+ <div class="title">{{if .Name}}{{.Name}}{{else}}(unnamed){{end}}</div>
405
</div>
380
- </div>
381
- {{if .DNS}}<div class="muted">DNS Label: <span class="mono">{{.DNS}}</span></div>{{end}}
382
- <div class="muted">Lease Identity</div>
383
- <div class="mono">{{.Peer}}</div>
384
- <div class="muted" style="margin-top:6px">Last seen: {{.LastSeen}}{{if .TTL}} - TTL: {{.TTL}}{{end}}</div>
385
- <a class="btn" href="{{.Link}}">Open</a>
386
- </section>
387
- {{else}}
388
- <section class="section">
389
- <div class="title">No clients discovered</div>
390
- <div class="muted">Start a client and ensure bootstrap URLs point at this server's /relay WebSocket endpoint.</div>
391
- </section>
392
- {{end}}
406
+ <div class="muted">Last seen: {{.LastSeen}}</div>
407
+ </a>
408
+ {{else}}
409
+ <article class="card">
410
+ <div class="title">No clients discovered</div>
411
+ <div class="muted">Start a client and ensure bootstrap URLs point at this server's /relay WebSocket endpoint.</div>
412
+ </article>
413
+ {{end}}
414
+ </div>
415
</main>
416
</div>
417
+ <script>
418
+ function filterCards(q) {
419
+ q = (q || '').toLowerCase().trim();
420
+ const cards = document.querySelectorAll('.grid .card');
421
+ cards.forEach(card => {
422
+ const name = (card.getAttribute('data-name') || '').toLowerCase();
423
+ const show = !q || name.includes(q);
424
+ card.style.display = show ? '' : 'none';
425
+ });
426
+ }
427
+ </script>
428
</body>
429
</html>`))