adminui: make the web UI usable on a phone

The header was a rigid flex row, so on a narrow screen the account name
and the sign-out button were pushed off the side of the viewport where
they could not be reached at all — not merely ugly, unusable. It wraps
now, and below 40rem the account block takes its own full-width row so a
long display name cannot crowd out the navigation.

Wide content scrolls inside its own box rather than dragging the page
sideways with it. Tables sit in an overflow-x container and <pre> is
capped at the viewport width; without that, one long self-test line or
one device table makes every other column of text unreadable, and on a
phone it is not obvious that the page has moved at all. Long opaque
strings — device ids, enrolment links — wrap anywhere rather than
insisting on a width nothing has.

Also: box-sizing on everything, stat cards that share a row instead of
each claiming the full width, and larger touch targets on buttons, where
.4rem is comfortable with a mouse and fiddly with a thumb.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-01 22:33:41 +02:00
co-authored by Claude Opus 5
parent 89b084338f
commit 621ee99b77
+36 -12
View File
@@ -39,29 +39,53 @@ const baseHTML = `<!doctype html>
<title>Echolot &mdash; {{.Page}}</title> <title>Echolot &mdash; {{.Page}}</title>
<style> <style>
:root{color-scheme:dark} :root{color-scheme:dark}
*{box-sizing:border-box}
body{font:15px/1.5 system-ui,sans-serif;margin:0;background:#14161a;color:#e6e6e6} body{font:15px/1.5 system-ui,sans-serif;margin:0;background:#14161a;color:#e6e6e6}
header{display:flex;gap:1.2rem;align-items:baseline;padding:.8rem 1.2rem;background:#1c1f25;border-bottom:1px solid #2b2f36} /* The header wraps rather than overflowing: on a phone a rigid flex row pushes the account name
and the sign-out button off the side of the screen, where they cannot be reached at all. */
header{display:flex;gap:.6rem 1.2rem;align-items:baseline;flex-wrap:wrap;
padding:.8rem 1.2rem;background:#1c1f25;border-bottom:1px solid #2b2f36}
header h1{font-size:1.1rem;margin:0;font-weight:600} header h1{font-size:1.1rem;margin:0;font-weight:600}
header nav a{color:#9ecbff;text-decoration:none;margin-right:1rem} header nav{display:flex;flex-wrap:wrap;gap:.2rem 1rem}
header .who{margin-left:auto;color:#9aa3ad;font-size:.9rem} header nav a{color:#9ecbff;text-decoration:none}
header .who{margin-left:auto;color:#9aa3ad;font-size:.9rem;
display:flex;align-items:center;gap:.6rem;flex-wrap:wrap}
main{padding:1.2rem;max-width:70rem} main{padding:1.2rem;max-width:70rem}
table{border-collapse:collapse;width:100%;margin:.6rem 0} table{border-collapse:collapse;width:100%;margin:0}
th,td{text-align:left;padding:.45rem .6rem;border-bottom:1px solid #2b2f36;vertical-align:top} th,td{text-align:left;padding:.45rem .6rem;border-bottom:1px solid #2b2f36;vertical-align:top}
th{color:#9aa3ad;font-weight:500;font-size:.85rem} th{color:#9aa3ad;font-weight:500;font-size:.85rem}
/* Wide content scrolls inside its own box. Letting the page scroll sideways instead makes every
other column of text unreadable, and on a phone it is not obvious that it happened. */
.tablewrap{overflow-x:auto;margin:.6rem 0;-webkit-overflow-scrolling:touch}
.tablewrap table{min-width:32rem}
code,pre{font-family:ui-monospace,monospace;font-size:.85rem} code,pre{font-family:ui-monospace,monospace;font-size:.85rem}
pre{background:#0f1114;padding:.8rem;border-radius:6px;overflow:auto;max-height:34rem} code{overflow-wrap:anywhere}
.card{background:#1c1f25;border:1px solid #2b2f36;border-radius:8px;padding:1rem;margin:.8rem 0} pre{background:#0f1114;padding:.8rem;border-radius:6px;overflow:auto;max-height:34rem;max-width:100%}
.card{background:#1c1f25;border:1px solid #2b2f36;border-radius:8px;padding:1rem;margin:.8rem 0;
min-width:0}
.grid{display:flex;gap:1rem;flex-wrap:wrap} .grid{display:flex;gap:1rem;flex-wrap:wrap}
.stat{background:#1c1f25;border:1px solid #2b2f36;border-radius:8px;padding:.8rem 1.2rem;min-width:8rem} .stat{background:#1c1f25;border:1px solid #2b2f36;border-radius:8px;padding:.8rem 1.2rem;
flex:1 1 8rem;min-width:8rem}
.stat b{display:block;font-size:1.6rem;font-weight:600} .stat b{display:block;font-size:1.6rem;font-weight:600}
.stat span{color:#9aa3ad;font-size:.85rem} .stat span{color:#9aa3ad;font-size:.85rem}
button{font:inherit;background:#2d6cdf;color:#fff;border:0;border-radius:6px;padding:.4rem .8rem;cursor:pointer} button{font:inherit;background:#2d6cdf;color:#fff;border:0;border-radius:6px;padding:.4rem .8rem;cursor:pointer}
button.danger{background:#8b2f2f} button.danger{background:#8b2f2f}
button.plain{background:#3a3f47} button.plain{background:#3a3f47}
input{font:inherit;background:#0f1114;color:#e6e6e6;border:1px solid #2b2f36;border-radius:6px;padding:.4rem .6rem} input{font:inherit;background:#0f1114;color:#e6e6e6;border:1px solid #2b2f36;border-radius:6px;
padding:.4rem .6rem;max-width:100%}
.err{background:#3a1f1f;border:1px solid #7a3b3b;padding:.6rem .8rem;border-radius:6px} .err{background:#3a1f1f;border:1px solid #7a3b3b;padding:.6rem .8rem;border-radius:6px}
.muted{color:#9aa3ad} .muted{color:#9aa3ad}
form.inline{display:inline} form.inline{display:inline}
@media (max-width:40rem){
main{padding:.9rem}
header{padding:.7rem .9rem}
/* Full width on its own row, so the name can be long without stealing the nav's space. */
header .who{margin-left:0;width:100%;justify-content:space-between}
/* Touch targets: .4rem of padding is comfortable with a mouse and fiddly with a thumb. */
button{padding:.5rem .9rem}
.stat{padding:.7rem .9rem}
.stat b{font-size:1.4rem}
}
</style></head><body> </style></head><body>
{{if ne .Page "login"}} {{if ne .Page "login"}}
<header> <header>
@@ -130,7 +154,7 @@ const baseHTML = `<!doctype html>
<button>Create enrolment link</button> <button>Create enrolment link</button>
</form> </form>
{{end}} {{end}}
<table> <div class="tablewrap"><table>
<tr><th>Device</th><th>Name</th><th>Account</th><th>Enrolled</th><th>Runs</th><th></th></tr> <tr><th>Device</th><th>Name</th><th>Account</th><th>Enrolled</th><th>Runs</th><th></th></tr>
{{range .Rows}} {{range .Rows}}
<tr> <tr>
@@ -146,13 +170,13 @@ const baseHTML = `<!doctype html>
{{else}} {{else}}
<tr><td colspan="6" class="muted">{{if $.Admin}}No devices enrolled.{{else}}You have not signed in on any device yet. Sign in from the Echolot app to link one.{{end}}</td></tr> <tr><td colspan="6" class="muted">{{if $.Admin}}No devices enrolled.{{else}}You have not signed in on any device yet. Sign in from the Echolot app to link one.{{end}}</td></tr>
{{end}} {{end}}
</table> </table></div>
{{else if eq .Page "runs"}} {{else if eq .Page "runs"}}
<h2>{{if .Admin}}Uploaded runs{{else}}Your uploaded runs{{end}}</h2> <h2>{{if .Admin}}Uploaded runs{{else}}Your uploaded runs{{end}}</h2>
<p class="muted">Shown exactly as uploaded, at the privacy level the uploader chose. Nothing <p class="muted">Shown exactly as uploaded, at the privacy level the uploader chose. Nothing
here can un-redact a run.</p> here can un-redact a run.</p>
<table> <div class="tablewrap"><table>
<tr><th>Uploaded</th><th>Device</th><th>Verdict</th><th>Findings</th><th>Size</th><th>Level</th><th></th></tr> <tr><th>Uploaded</th><th>Device</th><th>Verdict</th><th>Findings</th><th>Size</th><th>Level</th><th></th></tr>
{{range .Rows}} {{range .Rows}}
<tr> <tr>
@@ -167,7 +191,7 @@ const baseHTML = `<!doctype html>
{{else}} {{else}}
<tr><td colspan="7" class="muted">Nothing uploaded yet.</td></tr> <tr><td colspan="7" class="muted">Nothing uploaded yet.</td></tr>
{{end}} {{end}}
</table> </table></div>
{{else if eq .Page "run"}} {{else if eq .Page "run"}}
<h2>Run {{.ID}}</h2> <h2>Run {{.ID}}</h2>