Add Kamon logo as favicon and brand mark; fix MTU and UI issues
- Kamon logo (crest ring around the ghost) as favicon.svg, 180 px apple-touch-icon.png and the brand lockup in sidebar and login page, with the katakana reading ゴーストワイヤー - /favicon.ico redirects to /favicon.svg - Client configs no longer set an MTU; each device picks its own, as pivpn does. Server MTU changes no longer require reissued configs. - Fix "null" rendered on the dashboard, wrapping activity times, and show log lines as readable text instead of raw JSON Claude-Session: https://claude.ai/code/session_01RAnLbyQZ5ZTA7KqwXP98nw
This commit is contained in:
@@ -39,8 +39,10 @@ a:hover { color: var(--link-hover); }
|
|||||||
/* shell */
|
/* shell */
|
||||||
.shell { display: flex; flex-wrap: wrap; min-height: 100vh; }
|
.shell { display: flex; flex-wrap: wrap; min-height: 100vh; }
|
||||||
.side { flex: 1 1 230px; background: var(--ink); color: #c9c9c3; padding: 20px 12px; display: flex; flex-direction: column; gap: 2px; }
|
.side { flex: 1 1 230px; background: var(--ink); color: #c9c9c3; padding: 20px 12px; display: flex; flex-direction: column; gap: 2px; }
|
||||||
.brand { display: flex; align-items: center; gap: 10px; padding: 4px 10px 18px; color: #fff; font-family: var(--mono); font-weight: 500; font-size: 16px; }
|
.brand { display: flex; align-items: center; gap: 12px; padding: 4px 10px 18px; color: #fff; }
|
||||||
.mark { width: 22px; height: 22px; border-radius: 6px; background: var(--down); display: grid; place-items: center; color: #fff; }
|
.brand svg { flex: none; }
|
||||||
|
.brand .wm { font-family: var(--mono); font-weight: 600; font-size: 16px; letter-spacing: 0.06em; line-height: 1.2; }
|
||||||
|
.brand .kana { font-family: "Hiragino Sans", "Yu Gothic", "Noto Sans JP", system-ui, sans-serif; font-size: 10px; letter-spacing: 0.3em; color: #8d8e93; margin-top: 2px; }
|
||||||
.srv { display: flex; align-items: center; gap: 8px; margin: 0 6px 16px; padding: 10px; border: 1px solid #2c2d32; border-radius: 8px; font-size: 12px; color: #c9c9c3; }
|
.srv { display: flex; align-items: center; gap: 8px; margin: 0 6px 16px; padding: 10px; border: 1px solid #2c2d32; border-radius: 8px; font-size: 12px; color: #c9c9c3; }
|
||||||
.srv strong { color: #fff; font-weight: 500; }
|
.srv strong { color: #fff; font-weight: 500; }
|
||||||
.side a.nav { display: flex; align-items: center; gap: 12px; min-height: 44px; padding: 0 12px; border-radius: 8px; color: #c9c9c3; text-decoration: none; font-weight: 500; }
|
.side a.nav { display: flex; align-items: center; gap: 12px; min-height: 44px; padding: 0 12px; border-radius: 8px; color: #c9c9c3; text-decoration: none; font-weight: 500; }
|
||||||
@@ -206,7 +208,9 @@ dialog::backdrop { background: rgba(22, 23, 26, .55); }
|
|||||||
/* login */
|
/* login */
|
||||||
.loginpage { min-height: 100vh; display: grid; place-items: center; padding: 32px 16px; background: var(--ink); }
|
.loginpage { min-height: 100vh; display: grid; place-items: center; padding: 32px 16px; background: var(--ink); }
|
||||||
.loginbox { width: 100%; max-width: 380px; display: flex; flex-direction: column; gap: 20px; }
|
.loginbox { width: 100%; max-width: 380px; display: flex; flex-direction: column; gap: 20px; }
|
||||||
.loginbox .brand { justify-content: center; font-size: 18px; padding: 0; }
|
.loginbox .brand { justify-content: center; padding: 0; }
|
||||||
|
.loginbox .brand .wm { font-size: 20px; }
|
||||||
|
.loginbox .brand .kana { font-size: 11px; }
|
||||||
.loginform { background: var(--surface); border-radius: 14px; padding: 28px; display: flex; flex-direction: column; gap: 16px; }
|
.loginform { background: var(--surface); border-radius: 14px; padding: 28px; display: flex; flex-direction: column; gap: 16px; }
|
||||||
.loginform h1 { font-size: 20px; }
|
.loginform h1 { font-size: 20px; }
|
||||||
.loginform input { min-height: 44px; }
|
.loginform input { min-height: 44px; }
|
||||||
|
|||||||
@@ -48,9 +48,24 @@
|
|||||||
server: '<rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><path d="M7 7.5h.01M7 16.5h.01"/>',
|
server: '<rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><path d="M7 7.5h.01M7 16.5h.01"/>',
|
||||||
settings: '<path d="M4 6h10M18 6h2M4 12h4M12 12h8M4 18h12"/><circle cx="16" cy="6" r="2"/><circle cx="10" cy="12" r="2"/><circle cx="18" cy="18" r="2"/>',
|
settings: '<path d="M4 6h10M18 6h2M4 12h4M12 12h8M4 18h12"/><circle cx="16" cy="6" r="2"/><circle cx="10" cy="12" r="2"/><circle cx="18" cy="18" r="2"/>',
|
||||||
plus: '<path d="M12 5v14M5 12h14"/>',
|
plus: '<path d="M12 5v14M5 12h14"/>',
|
||||||
mark: '<path d="M4 12h16M12 4v16"/>',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The Kamon logo: a crest ring around the ghost. Same drawing as favicon.svg.
|
||||||
|
const LOGO = '<rect width="64" height="64" rx="14" fill="#1b1b1d"/><rect x="0.5" y="0.5" width="63" height="63" rx="13.5" fill="none" stroke="#fff" stroke-opacity="0.2"/>' +
|
||||||
|
'<circle cx="32" cy="32" r="22" fill="none" stroke="#fff" stroke-width="3.5"/><g transform="translate(32 33) scale(0.66) translate(-32 -33)">' +
|
||||||
|
'<path d="M18 50V30a14 14 0 0 1 28 0v20l-4.7-4-4.6 4-4.7-4-4.7 4-4.6-4z" fill="#fff"/><circle cx="27" cy="30" r="3.2" fill="#c8372d"/><circle cx="37" cy="30" r="3.2" fill="#c8372d"/></g>';
|
||||||
|
|
||||||
|
function logo(size) {
|
||||||
|
const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
|
for (const [k, v] of Object.entries({ width: size, height: size, viewBox: '0 0 64 64', 'aria-hidden': 'true' })) s.setAttribute(k, v);
|
||||||
|
s.innerHTML = LOGO;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// brand is the logo lockup: mark, wordmark and the katakana reading.
|
||||||
|
const brand = (size) => h('div', { class: 'brand' }, logo(size),
|
||||||
|
h('div', null, h('div', { class: 'wm' }, APP), h('div', { class: 'kana', lang: 'ja' }, 'ゴーストワイヤー')));
|
||||||
|
|
||||||
// Icons are constant markup from ICONS, never data.
|
// Icons are constant markup from ICONS, never data.
|
||||||
function icon(name, size = 18, width = 1.7) {
|
function icon(name, size = 18, width = 1.7) {
|
||||||
const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
@@ -274,7 +289,7 @@
|
|||||||
peerCount = h('span', { class: 'count' });
|
peerCount = h('span', { class: 'count' });
|
||||||
navLinks = {};
|
navLinks = {};
|
||||||
const nav = h('nav', { class: 'side', 'aria-label': 'Main' },
|
const nav = h('nav', { class: 'side', 'aria-label': 'Main' },
|
||||||
h('div', { class: 'brand' }, h('span', { class: 'mark' }, icon('mark', 14, 2.4)), APP),
|
brand(34),
|
||||||
srvBox,
|
srvBox,
|
||||||
NAV.map(([href, ic, label]) => (navLinks[href] = h('a', { class: 'nav', href }, icon(ic), label, ic === 'peers' ? peerCount : null))),
|
NAV.map(([href, ic, label]) => (navLinks[href] = h('a', { class: 'nav', href }, icon(ic), label, ic === 'peers' ? peerCount : null))),
|
||||||
h('div', { class: 'foot' },
|
h('div', { class: 'foot' },
|
||||||
@@ -375,7 +390,7 @@
|
|||||||
err, btn,
|
err, btn,
|
||||||
h('p', { class: 'hint' }, 'After 5 failed attempts, sign-in is locked for 15 minutes.'));
|
h('p', { class: 'hint' }, 'After 5 failed attempts, sign-in is locked for 15 minutes.'));
|
||||||
app.replaceChildren(h('div', { class: 'loginpage' }, h('div', { class: 'loginbox' },
|
app.replaceChildren(h('div', { class: 'loginpage' }, h('div', { class: 'loginbox' },
|
||||||
h('div', { class: 'brand' }, h('span', { class: 'mark' }, icon('mark', 14, 2.4)), APP),
|
brand(44),
|
||||||
form)));
|
form)));
|
||||||
pw.focus();
|
pw.focus();
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><title>GHOSTWIRE</title><rect width="64" height="64" rx="14" fill="#1b1b1d"/><rect x="0.5" y="0.5" width="63" height="63" rx="13.5" fill="none" stroke="#fff" stroke-opacity="0.2"/><circle cx="32" cy="32" r="22" fill="none" stroke="#fff" stroke-width="3.5"/><g transform="translate(32 33) scale(0.66) translate(-32 -33)"><path d="M18 50V30a14 14 0 0 1 28 0v20l-4.7-4-4.6 4-4.7-4-4.7 4-4.6-4z" fill="#fff"/><circle cx="27" cy="30" r="3.2" fill="#c8372d"/><circle cx="37" cy="30" r="3.2" fill="#c8372d"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 572 B |
@@ -5,6 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="referrer" content="no-referrer">
|
<meta name="referrer" content="no-referrer">
|
||||||
<title>GHOSTWIRE</title>
|
<title>GHOSTWIRE</title>
|
||||||
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||||
<link rel="stylesheet" href="/app.css">
|
<link rel="stylesheet" href="/app.css">
|
||||||
<script src="/app.js" defer></script>
|
<script src="/app.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -5,19 +5,22 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The web UI is three static files built into the binary. It talks only to
|
// The web UI and its icons are built into the binary. The UI talks only to
|
||||||
// /api/v1, the same API the iOS app uses.
|
// /api/v1, the same API the iOS app uses.
|
||||||
//
|
//
|
||||||
//go:embed index.html app.js app.css
|
//go:embed index.html app.js app.css favicon.svg apple-touch-icon.png
|
||||||
var webFiles embed.FS
|
var webFiles embed.FS
|
||||||
|
|
||||||
func webHandler() http.Handler {
|
func webHandler() http.Handler {
|
||||||
files := http.FileServerFS(webFiles)
|
files := http.FileServerFS(webFiles)
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/", "/app.js", "/app.css":
|
case "/", "/app.js", "/app.css", "/favicon.svg", "/apple-touch-icon.png":
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
files.ServeHTTP(w, r)
|
files.ServeHTTP(w, r)
|
||||||
|
case "/favicon.ico":
|
||||||
|
// Browsers ask for this by default; point them to the SVG.
|
||||||
|
http.Redirect(w, r, "/favicon.svg", http.StatusMovedPermanently)
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user