From b394a6d5d9085a8008dad18658c22dad4b58c31b Mon Sep 17 00:00:00 2001 From: Daniel Redetzke Date: Sat, 3 Oct 2026 17:12:27 +0300 Subject: [PATCH] Add Kamon logo as favicon and brand mark; fix MTU and UI issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app.css | 10 +++++++--- app.js | 21 ++++++++++++++++++--- apple-touch-icon.png | Bin 0 -> 6455 bytes favicon.svg | 1 + index.html | 2 ++ web.go | 9 ++++++--- 6 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 apple-touch-icon.png create mode 100644 favicon.svg diff --git a/app.css b/app.css index c612581..7e75d52 100644 --- a/app.css +++ b/app.css @@ -39,8 +39,10 @@ a:hover { color: var(--link-hover); } /* shell */ .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; } -.brand { display: flex; align-items: center; gap: 10px; padding: 4px 10px 18px; color: #fff; font-family: var(--mono); font-weight: 500; font-size: 16px; } -.mark { width: 22px; height: 22px; border-radius: 6px; background: var(--down); display: grid; place-items: center; color: #fff; } +.brand { display: flex; align-items: center; gap: 12px; padding: 4px 10px 18px; 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 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; } @@ -206,7 +208,9 @@ dialog::backdrop { background: rgba(22, 23, 26, .55); } /* login */ .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 .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 h1 { font-size: 20px; } .loginform input { min-height: 44px; } diff --git a/app.js b/app.js index 77be5cb..c9c1735 100644 --- a/app.js +++ b/app.js @@ -48,9 +48,24 @@ server: '', settings: '', plus: '', - mark: '', }; + // The Kamon logo: a crest ring around the ghost. Same drawing as favicon.svg. + const LOGO = '' + + '' + + ''; + + 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. function icon(name, size = 18, width = 1.7) { const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); @@ -274,7 +289,7 @@ peerCount = h('span', { class: 'count' }); navLinks = {}; 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, NAV.map(([href, ic, label]) => (navLinks[href] = h('a', { class: 'nav', href }, icon(ic), label, ic === 'peers' ? peerCount : null))), h('div', { class: 'foot' }, @@ -375,7 +390,7 @@ err, btn, 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' }, - h('div', { class: 'brand' }, h('span', { class: 'mark' }, icon('mark', 14, 2.4)), APP), + brand(44), form))); pw.focus(); } diff --git a/apple-touch-icon.png b/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..dacf9397daff5696eb2a972a16db642d04fa9712 GIT binary patch literal 6455 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 diff --git a/index.html b/index.html index 0b3ee7f..335f9e7 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,8 @@ GHOSTWIRE + + diff --git a/web.go b/web.go index e200e7e..67262a5 100644 --- a/web.go +++ b/web.go @@ -5,19 +5,22 @@ import ( "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. // -//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 func webHandler() http.Handler { files := http.FileServerFS(webFiles) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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") 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: http.NotFound(w, r) }