Skip to content

Commit e0dadc0

Browse files
tridgeclaude
andcommitted
webadmin: ArduPilot logo header, password show/hide eye, static cache
* Vendored static/ardupilot_logo.png (245x88 RGBA, 7 KiB) and a blue (#3a7cb3) header bar that extends to the page edges so the transparent logo PNG visually merges with the bar. Logo and title share one .brand <a> back to /; nav links are inverted to white; the log-out button is white-on-blue with a hover wash. * Per-passphrase show/hide eye via static/password-toggle.js. JS wraps every <input type=password> on the page (idempotent via a data-attribute marker) with an SVG eye button that toggles input.type between password and text. tabindex=-1 keeps the eye out of the form's tab order; aria-label flips with state. * autocomplete=new-password on every PasswordField except the /login one (which keeps current-password). Without this Chrome pre-fills the 'New passphrase' fields on /admin/<port2>/ from the credential it has stored for /login. spellcheck/autocorrect /autocapitalize=off complete the picture. * SEND_FILE_MAX_AGE_DEFAULT=86400 so static assets carry Cache-Control: public, max-age=86400. Without this Flask emits no cache header, the meta-refresh flow re-fetches the logo every 5 s, and the header flashes mid-paint. Explicit width/height + fetchpriority=high on the <img> tag let the browser carve out the slot at parse time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5ed4415 commit e0dadc0

6 files changed

Lines changed: 128 additions & 12 deletions

File tree

webadmin/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,10 @@ class DefaultConfig:
4545
# Site-name shown in templates. Overridden by webui.json's "title"
4646
# field if that file exists in the keys.tdb directory.
4747
WEBUI_TITLE = 'SupportProxy admin'
48+
49+
# Browser-cache static assets (logo, CSS, JS). Without this Flask
50+
# emits no Cache-Control header, so the meta-refresh flow refetches
51+
# the logo every 5 s and you see a blank header flash mid-paint.
52+
# Set to 1 day; bust by renaming files or appending a query string
53+
# if you change one.
54+
SEND_FILE_MAX_AGE_DEFAULT = 86400

webadmin/forms.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,24 @@
1010
OWNER_MAX_TLOG_RETENTION_DAYS = 30.0
1111
ADMIN_MAX_TLOG_RETENTION_DAYS = 36500.0 # ~100 years; effectively unbounded
1212

13+
# Render-kwargs for "this is a brand-new passphrase, browser, don't
14+
# autofill the user's saved login passphrase here". Without this Chrome
15+
# happily prefills the 'New passphrase' field with the value it has
16+
# stored for /login on this site.
17+
_NEW_PW_KW = {'autocomplete': 'new-password', 'spellcheck': 'false',
18+
'autocorrect': 'off', 'autocapitalize': 'off'}
19+
_CURRENT_PW_KW = {'autocomplete': 'current-password', 'spellcheck': 'false',
20+
'autocorrect': 'off', 'autocapitalize': 'off'}
21+
1322

1423
class LoginForm(FlaskForm):
1524
port = IntegerField('Port (port1 or port2)',
1625
validators=[DataRequired(),
1726
NumberRange(min=1, max=65535)])
1827
passphrase = PasswordField('Passphrase',
1928
validators=[DataRequired(),
20-
Length(min=1, max=256)])
29+
Length(min=1, max=256)],
30+
render_kw=_CURRENT_PW_KW)
2131
submit = SubmitField('Log in')
2232

2333

@@ -26,11 +36,13 @@ class OwnerEditForm(FlaskForm):
2636
name = StringField('Display name', validators=[Optional(), Length(max=31)])
2737
new_passphrase = PasswordField(
2838
'New passphrase (leave blank to keep current)',
29-
validators=[Optional(), Length(min=4, max=256)])
39+
validators=[Optional(), Length(min=4, max=256)],
40+
render_kw=_NEW_PW_KW)
3041
confirm_passphrase = PasswordField(
3142
'Confirm new passphrase',
3243
validators=[Optional(), EqualTo('new_passphrase',
33-
message='Passphrases do not match.')])
44+
message='Passphrases do not match.')],
45+
render_kw=_NEW_PW_KW)
3446
bidi_sign = BooleanField(
3547
'Require MAVLink signing on the user side too (bi-directional signing)')
3648
tlog_enabled = BooleanField('Record telemetry logs (.tlog) for this entry')
@@ -50,11 +62,13 @@ class AdminEditForm(FlaskForm):
5062
NumberRange(min=1, max=65535)])
5163
new_passphrase = PasswordField(
5264
'New passphrase (leave blank to keep current)',
53-
validators=[Optional(), Length(min=4, max=256)])
65+
validators=[Optional(), Length(min=4, max=256)],
66+
render_kw=_NEW_PW_KW)
5467
confirm_passphrase = PasswordField(
5568
'Confirm new passphrase',
5669
validators=[Optional(), EqualTo('new_passphrase',
57-
message='Passphrases do not match.')])
70+
message='Passphrases do not match.')],
71+
render_kw=_NEW_PW_KW)
5872
is_admin = BooleanField('Grant admin privilege (KEY_FLAG_ADMIN)')
5973
bidi_sign = BooleanField(
6074
'Require MAVLink signing on the user side too (bi-directional signing)')
@@ -78,7 +92,8 @@ class AdminAddForm(FlaskForm):
7892
validators=[DataRequired(), Length(max=31)])
7993
passphrase = PasswordField('Passphrase',
8094
validators=[DataRequired(),
81-
Length(min=4, max=256)])
95+
Length(min=4, max=256)],
96+
render_kw=_NEW_PW_KW)
8297
submit = SubmitField('Add')
8398

8499

webadmin/static/ardupilot_logo.png

7.18 KB
Loading

webadmin/static/password-toggle.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
Auto-attach a show/hide eye button to every <input type="password"> on
3+
the page. Idempotent: re-running (e.g. after a partial DOM update)
4+
skips inputs already wrapped.
5+
6+
No deps: written so it works without a build step.
7+
*/
8+
(function () {
9+
'use strict';
10+
11+
var EYE_OPEN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
12+
var EYE_SHUT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>';
13+
14+
function attach(input) {
15+
if (input.dataset.toggleAttached) {
16+
return;
17+
}
18+
input.dataset.toggleAttached = '1';
19+
20+
var wrap = document.createElement('span');
21+
wrap.className = 'password-wrap';
22+
input.parentNode.insertBefore(wrap, input);
23+
wrap.appendChild(input);
24+
25+
var btn = document.createElement('button');
26+
btn.type = 'button';
27+
btn.className = 'toggle';
28+
// The eye is a convenience — keep it out of the tab order so
29+
// tabbing through forms doesn't land on it.
30+
btn.tabIndex = -1;
31+
btn.setAttribute('aria-label', 'Show passphrase');
32+
btn.innerHTML = EYE_OPEN;
33+
btn.addEventListener('click', function () {
34+
var revealing = input.type === 'password';
35+
input.type = revealing ? 'text' : 'password';
36+
btn.setAttribute(
37+
'aria-label', revealing ? 'Hide passphrase' : 'Show passphrase');
38+
btn.innerHTML = revealing ? EYE_SHUT : EYE_OPEN;
39+
});
40+
wrap.appendChild(btn);
41+
}
42+
43+
function init() {
44+
var inputs = document.querySelectorAll('input[type=password]');
45+
for (var i = 0; i < inputs.length; i++) {
46+
attach(inputs[i]);
47+
}
48+
}
49+
50+
if (document.readyState === 'loading') {
51+
document.addEventListener('DOMContentLoaded', init);
52+
} else {
53+
init();
54+
}
55+
})();

webadmin/static/style.css

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
html { font-family: system-ui, sans-serif; color: #222; background: #fafafa; }
22
body { max-width: 960px; margin: 0 auto; padding: 1rem; }
3-
header { display: flex; justify-content: space-between; align-items: baseline;
4-
border-bottom: 1px solid #ccc; padding-bottom: 0.5rem; margin-bottom: 1rem; }
5-
header h1 { font-size: 1.2rem; margin: 0; }
6-
header h1 a { color: inherit; text-decoration: none; }
7-
header nav { display: flex; gap: 1rem; align-items: baseline; font-size: 0.9rem; }
3+
/* ArduPilot-blue header bar so the logo's transparent PNG blends in. */
4+
header { display: flex; justify-content: space-between; align-items: center;
5+
background: #3a7cb3; color: #fff;
6+
border-bottom: 1px solid #2a5e88;
7+
padding: 0.4rem 0.8rem; margin: -1rem -1rem 1rem; }
8+
header h1 { font-size: 1.2rem; margin: 0; color: #fff; }
9+
header .brand { display: flex; align-items: center; gap: 0.6rem;
10+
text-decoration: none; color: inherit; }
11+
header .brand .logo { height: 2.4rem; width: auto; display: block; }
12+
header nav { display: flex; gap: 1rem; align-items: center; font-size: 0.9rem; }
13+
header nav a { color: #fff; }
814
header nav form.inline { display: inline; }
15+
header nav button { background: #fff; color: #3a7cb3; border: 1px solid #2a5e88; }
16+
header nav button:hover { background: #e6f0fa; }
917
main { padding: 0.5rem 0; }
1018

1119
.field { margin: 0.5rem 0; }
@@ -47,3 +55,28 @@ footer.version {
4755
}
4856
footer.version a { color: #888; text-decoration: none; }
4957
footer.version a:hover { text-decoration: underline; }
58+
59+
/* Show/hide eye toggle attached to every password input by
60+
static/password-toggle.js. The button sits over the input's right
61+
edge; we extend the input's right padding so the text doesn't run
62+
under the icon. The selector covers both type=password (default)
63+
and type=text (after revealing) since the JS toggles the type. */
64+
.password-wrap { display: inline-block; position: relative; vertical-align: middle; }
65+
.password-wrap input[type=password],
66+
.password-wrap input[type=text] {
67+
padding-right: 2.2rem;
68+
}
69+
.password-wrap button.toggle {
70+
position: absolute;
71+
right: 0.3rem;
72+
top: 50%;
73+
transform: translateY(-50%);
74+
background: transparent;
75+
border: none;
76+
padding: 0.15rem;
77+
cursor: pointer;
78+
color: #666;
79+
line-height: 0;
80+
}
81+
.password-wrap button.toggle:hover { color: #222; }
82+
.password-wrap button.toggle svg { width: 18px; height: 18px; display: block; }

webadmin/templates/base.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
</head>
99
<body>
1010
<header>
11-
<h1><a href="{{ url_for('index') }}">{{ config.WEBUI_TITLE }}</a></h1>
11+
<a class="brand" href="{{ url_for('index') }}">
12+
<img src="{{ url_for('static', filename='ardupilot_logo.png') }}"
13+
alt="ArduPilot" class="logo"
14+
width="245" height="88" fetchpriority="high">
15+
<h1>{{ config.WEBUI_TITLE }}</h1>
16+
</a>
1217
<nav>
1318
{% if session.get('owner') %}
1419
<span>logged in as port {{ session['owner'] }}{% if session.get('is_admin') %} (admin){% endif %}</span>
@@ -39,6 +44,7 @@ <h1><a href="{{ url_for('index') }}">{{ config.WEBUI_TITLE }}</a></h1>
3944
target="_blank" rel="noopener">{{ config.GIT_VERSION }}</a>
4045
</footer>
4146
{% endif %}
47+
<script src="{{ url_for('static', filename='password-toggle.js') }}" defer></script>
4248
<script src="{{ url_for('static', filename='localtime.js') }}" defer></script>
4349
</body>
4450
</html>

0 commit comments

Comments
 (0)