README: add portal thumbnail

Kim committed Oct 30, 2025 at 11:36 UTC d82bf216cfc92465a03c818a0c9cf9672763cb8c
3 files changed +13 -7
README.md
+3 -1
@@ -1,5 +1,7 @@
1 # PORTAL — Publicly Open Relay To Access Localhost
2 -> Run locally, host everywhere!
2 +<p align="center">
3 + <img src="portal.jpg" alt="Portal logo" width="540" />
4 +</p>
5
6 Portal is a secure, encrypted relay service that enables end-to-end encrypted communication between clients through a central relay server. It provides mutual authentication, forward secrecy, and secure connection management with cryptographic identity verification.
7
cmd/relay-server/view.go
+10 -6
@@ -333,7 +333,7 @@ var serverTmpl = template.Must(template.New("admin-index").Parse(`<!doctype html
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; 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 }
336 + .brand { display:flex; align-items:center; height:36px; font-weight:800; font-size:22px; 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 }
@@ -342,6 +342,8 @@ var serverTmpl = template.Must(template.New("admin-index").Parse(`<!doctype html
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 + .chevron { margin-left:auto; width:16px; height:16px; color:var(--muted); opacity:.9; transition: transform .2s ease, color .2s ease, opacity .2s ease }
346 + .card:hover .chevron { transform: translateX(2px); color:var(--primary); opacity:1 }
347 .status-dot { width:10px; height:10px; border-radius:999px; background:var(--muted) }
348 .status-dot.ok { background: var(--ok) }
349 .status-dot.bad { background: var(--bad) }
@@ -357,6 +359,7 @@ var serverTmpl = template.Must(template.New("admin-index").Parse(`<!doctype html
359 .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 }
360 .btn.outline { background:transparent; color:var(--primary); border:1px solid var(--primary) }
361 .actions { margin-top:auto; display:flex; justify-content:flex-end }
362 + .card:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px }
363 /* Google-like search bar */
364 .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 }
365 .searchbar:focus-within { box-shadow: 0 1px 6px rgba(32,33,36,.28); border-color:#dfe1e5 }
@@ -396,23 +399,24 @@ var serverTmpl = template.Must(template.New("admin-index").Parse(`<!doctype html
399 <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"/>
400 </svg>
401 </a>
399 - <div class="counter" aria-label="Active clients" title="Active clients">
402 + <div class="counter" aria-label="Active devices" title="Active devices">
403 <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
401 - <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"/>
404 + <circle cx="12" cy="12" r="6"/>
405 </svg>
406 <span>{{len .Rows}}</span>
407 </div>
408 </div>
409 </header>
410 <main>
408 -
409 -
411 <div class="grid">
412 {{range .Rows}}
412 - <a class="card {{if .Connected}}connected{{else}}disconnected{{end}}" id="peer-{{.Peer}}" data-peer="{{.Peer}}" data-name="{{.Name}}" href="{{.Link}}">
413 + <a class="card {{if .Connected}}connected{{else}}disconnected{{end}}" id="peer-{{.Peer}}" data-peer="{{.Peer}}" data-name="{{.Name}}" href="{{.Link}}" title="{{if .Name}}{{.Name}}{{else}}(unnamed){{end}} 열기">
414 <div class="card-head">
415 <span class="status-dot {{if .Connected}}ok{{else}}bad{{end}}"></span>
416 <div class="title">{{if .Name}}{{.Name}}{{else}}(unnamed){{end}}</div>
417 + <svg class="chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
418 + <path d="M9 6l6 6-6 6"/>
419 + </svg>
420 </div>
421 <div class="muted">Last seen: {{.LastSeen}}</div>
422 </a>
portal.jpg
Binary files /dev/null and b/portal.jpg differ