Skip to content
Merged
158 changes: 158 additions & 0 deletions static/css/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -547,3 +547,161 @@
.hw-nav-item.hw-nav-unavailable:hover {
opacity: 0.6;
}

/* === AUTH GATE === */

/* The token prompt is the first thing a v2 install shows anyone, and it
shipped with no rules at all: a bare heading, an unstyled input and a
default browser button, rendered in page flow underneath the sidebar and
clipped off the viewport. It is a gate, not a panel, so it covers the app
instead of sitting inside it. */
.hw-auth-prompt {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bump the cache keys for the changed frontend assets

On upgrades where a browser has cached /css/components.css?v=1.0.0 or /js/app.js?v=1.0.0, this commit keeps those same URLs in static/index.html, so the browser can reuse the previous files. That leaves the token gate unstyled—the regression this patch is intended to fix—and can leave the newly emptied version label blank because the cached app script lacks showAgentVersion; change the query versions for both modified assets.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both changed assets now load as ?v=2.0.3 and the pinned components.css digest is updated. Fixed in b2c10c3.

position: fixed;
inset: 0;
Comment on lines +559 to +560

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent focus from escaping behind the token gate

When a signed-out keyboard user tabs past the token input and Retry button, focus proceeds to the sidebar or mobile navigation even though this fixed overlay visually covers those controls. The user is then operating concealed links, and activating one can invoke the already-bound hash router while authentication is still pending; make the rest of the app inert or trap focus within the prompt until it closes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gate's siblings (sidebar, content, mobile nav) now go inert while the prompt is visible and recover on close, and auth.js gets a new cache key. Verified in the running app: a simulated tab walk from the token input cycles only between the input and Retry. Fixed in HEAD.

/* Above every layout layer: the sidebar and mobile nav both sit at 100,
and the mobile nav follows the gate in the DOM, so an equal level would
paint tappable navigation on top of a signed-out screen. */
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
padding: var(--hw-space-md);
background: var(--hw-bg-primary);
background: color-mix(in srgb, var(--hw-bg-primary) 92%, transparent);
}

/* display:flex above beats the hidden attribute's UA display:none, so without
this the gate is drawn permanently and nothing behind it is reachable. */
.hw-auth-prompt[hidden] {
display: none;
}

.hw-auth-prompt form {
width: 100%;
max-width: 24rem;
display: flex;
flex-direction: column;
gap: var(--hw-space-md);
padding: var(--hw-space-lg);
/* bg-card only exists in the light and terminal themes. Without the
fallback the default theme resolves it to nothing and the card loses its
background entirely against the overlay behind it. */
background: var(--hw-bg-card, var(--hw-bg-secondary));
border: 1px solid var(--hw-border-color);
border-radius: var(--hw-border-radius-md);
box-shadow: var(--hw-shadow-card);
}

.hw-auth-prompt h2 {
margin: 0;
font-family: var(--hw-font-display);
font-size: var(--hw-font-size-lg);
font-weight: 500;
/* 19.2px at weight 500 is not WCAG large text, and the light theme's
accent blue only reaches 3.7:1 there, so themes may override. */
color: var(--hw-auth-heading, var(--hw-accent-primary));
}

.hw-auth-prompt label {
font-family: var(--hw-font-display);
font-size: var(--hw-font-size-xs);
letter-spacing: 0.08em;
text-transform: uppercase;
/* Small uppercase text needs 4.5:1, and the tertiary tone misses that in
every theme. The credential field's name is not decoration. */
color: var(--hw-text-secondary);
margin-bottom: calc(-1 * var(--hw-space-sm));
}

.hw-auth-prompt input {
width: 100%;
box-sizing: border-box;
font-family: var(--hw-font-display);
font-size: var(--hw-font-size-md);
padding: var(--hw-space-sm) var(--hw-space-md);
background: var(--hw-bg-primary);
/* The stock border tone sits under 1.7:1 on the input fill in every
theme, and a control a signed-out user cannot find is a locked door.
The secondary text tone clears the 3:1 non-text floor everywhere. */
border: 1px solid var(--hw-auth-input-border, var(--hw-text-secondary));
border-radius: var(--hw-border-radius-sm);
color: var(--hw-text-primary);
transition: border-color var(--hw-transition-fast);
}

.hw-auth-prompt input:focus {
outline: none;
border-color: var(--hw-accent-primary);
}

/* Outlined rather than filled, and measured rather than assumed. A filled
accent button is unreadable in the light theme: accent #3b82f6 behind
bg-primary #f5f3ef is 3.32:1, under the 4.5:1 AA floor. --hw-accent-primary-dim
is worse, since neither alternate theme redefines it and both inherit the
base theme's dark olive. Body text on the card is the one pairing every
theme has to keep readable, and it measures 14.94 / 13.82 / 14.50 across
default, light and terminal. The accent carries the emphasis as a border,
where contrast against text does not matter. */
.hw-auth-prompt button {
font-family: var(--hw-font-display);
font-size: var(--hw-font-size-sm);
padding: var(--hw-space-sm) var(--hw-space-md);
background: transparent;
border: 1px solid var(--hw-accent-primary);
border-radius: var(--hw-border-radius-sm);
color: var(--hw-text-primary);
cursor: pointer;
transition: background-color var(--hw-transition-fast);
}

.hw-auth-prompt button:hover {
background: var(--hw-bg-hover);
}

.hw-auth-prompt button:focus-visible {
outline: 2px solid var(--hw-accent-primary);
outline-offset: 2px;
}

/* role="alert" with no styling reads as body copy, which is the one thing an
error must not do. */
.hw-auth-prompt #auth-error {
margin: 0;
font-size: var(--hw-font-size-sm);
color: var(--hw-auth-error, var(--hw-status-critical));
}

.hw-auth-prompt #auth-error[hidden] {
display: none;
}

/* === AUTH STATUS (sidebar) === */

.hw-auth-status {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--hw-space-sm);
padding: var(--hw-space-sm) var(--hw-space-md);
font-size: var(--hw-font-size-xs);
/* Status and logout are meaningful labels, and the tertiary tone fails
4.5:1 on every sidebar. */
color: var(--hw-text-secondary);
}

.hw-auth-status button {
font-family: var(--hw-font-display);
font-size: var(--hw-font-size-xs);
padding: var(--hw-space-xs) var(--hw-space-sm);
background: transparent;
border: 1px solid var(--hw-border-color);
border-radius: var(--hw-border-radius-sm);
color: var(--hw-text-secondary);
cursor: pointer;
transition: all var(--hw-transition-fast);
}

.hw-auth-status button:hover {
border-color: var(--hw-accent-primary);
color: var(--hw-text-primary);
}
9 changes: 9 additions & 0 deletions static/css/themes/theme-light.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@
--hw-status-ok: #22c55e;
--hw-status-warn: #f59e0b;
--hw-status-critical: #ef4444;
/* The gate's error text is small copy on a near-white form, where the
status red only reaches about 3.8:1. Darker red for that one surface. */
--hw-auth-error: #b91c1c;
/* Same story for the gate heading: 19.2px at weight 500 is not large
text, and the accent blue stops at 3.7:1 on the white form. */
--hw-auth-heading: #1d4ed8;
/* And the token input's resting border: the light secondary gray only
reaches 2.6:1 on the near-white fill, under the 3:1 non-text floor. */
--hw-auth-input-border: #6b7280;
--hw-status-offline: #94a3b8;

/* Borders */
Expand Down
10 changes: 5 additions & 5 deletions static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<!-- CSS -->
<link rel="stylesheet" href="/css/theme.css?v=1.0.0">
<link rel="stylesheet" href="/css/layout.css?v=1.0.0">
<link rel="stylesheet" href="/css/components.css?v=1.0.0">
<link rel="stylesheet" href="/css/components.css?v=2.0.3">
<link id="theme-override" rel="stylesheet" href="">
</head>
<body>
Expand Down Expand Up @@ -78,7 +78,7 @@ <h1>HumWatch</h1>
</div>

<div class="sidebar-footer">
<span class="version-label" id="app-version">v1.0.0</span>
<span class="version-label" id="app-version"></span>
</div>

<div class="hw-sse-status" id="sse-status">
Expand Down Expand Up @@ -148,10 +148,10 @@ <h2 id="auth-title">HumWatch token required</h2>
<script src="/js/utils/security.js?v=1.0.0"></script>
<script src="/js/utils/format.js?v=1.0.0"></script>
<script src="/js/utils/time.js?v=1.0.0"></script>
<script src="/js/utils/theme.js?v=1.0.0"></script>
<script src="/js/utils/theme.js?v=2.0.3"></script>

<!-- JS Core -->
<script src="/js/auth.js?v=1.0.0"></script>
<script src="/js/auth.js?v=2.0.3"></script>
<script src="/js/api.js?v=1.0.0"></script>
<script src="/js/sse.js?v=1.0.0"></script>
<script src="/js/charts.js?v=1.0.0"></script>
Expand All @@ -170,6 +170,6 @@ <h2 id="auth-title">HumWatch token required</h2>
<script src="/js/pages/settings.js?v=1.0.0"></script>

<!-- App Router (must be last) -->
<script src="/js/app.js?v=1.0.0"></script>
<script src="/js/app.js?v=2.0.3"></script>
</body>
</html>
18 changes: 18 additions & 0 deletions static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ HumWatch.router._authenticateThenStart = function() {
};

HumWatch.router._startProtectedDashboard = function() {
// The rate-limited probe path reaches here with the gate still up.
HumWatch.auth.closePrompt();
HumWatch.router._dashboardStarted = true;

// Load config for alert thresholds
Expand Down Expand Up @@ -193,6 +195,21 @@ HumWatch.router._startProtectedDashboard = function() {
};

(function() {
// The sidebar version was hard-coded in index.html and nothing ever
// updated it, so every install advertised whatever string shipped in the
// markup no matter which agent was actually running. Health is public, so
// this resolves on the token screen too, before anyone has authenticated.
function showAgentVersion() {
var label = document.getElementById('app-version');
if (!label) return;
HumWatch.api.getHealth().then(function(health) {
if (health && health.version) label.textContent = 'v' + health.version;
}).catch(function() {
// An unreachable agent has no version to report. Saying nothing
// beats naming a version we never got an answer from.
});
}

// Wait for DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
Expand All @@ -202,6 +219,7 @@ HumWatch.router._startProtectedDashboard = function() {

function init() {
HumWatch.auth.init();
showAgentVersion();
HumWatch.auth._onAuthenticated = function() {
HumWatch.router._authenticateThenStart();
};
Expand Down
26 changes: 25 additions & 1 deletion static/js/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,34 @@ HumWatch.auth._setState = function(label) {
if (state) state.textContent = label;
};

// Every path that starts the dashboard goes through this, because a gate left
// visible after authentication succeeds now takes the inert app down with it.
HumWatch.auth.closePrompt = function() {
HumWatch.auth._promptOpen = false;
HumWatch.auth._cancelRetry();
HumWatch.auth._setPromptVisible(false);
};
Comment on lines +94 to +98

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear the rate-limit error when closing the gate

When an automatic retry succeeds after handleRateLimited, this hides the prompt but leaves #auth-error visible with the old “retrying” message. If the user later logs out or receives another 401, requireToken() reopens the prompt without clearing that error, falsely claiming that a retry is scheduled; reset the error text/hidden state when closing or reopening the prompt.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_setPromptVisible now resets the error text and hidden state whenever the prompt shows. Both writers (the rate-limit notice and the submit failure) run after that reset, verified live: rate-limit message shows, gate closes on success, and the reopened prompt is clean. Fixed in HEAD.


HumWatch.auth._setPromptVisible = function(visible) {
var prompt = document.getElementById('auth-prompt');
if (prompt) prompt.hidden = !visible;
if (prompt) {
prompt.hidden = !visible;
// The gate paints above the app, but tab order ignores paint order:
// without this, focus walks into the concealed sidebar and nav, where
// Enter drives the hash router while authentication is still pending.
var siblings = prompt.parentElement ? prompt.parentElement.children : [];
for (var i = 0; i < siblings.length; i++) {
if (siblings[i] !== prompt) siblings[i].inert = visible;
}
Comment on lines +107 to +110

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Release the gate after a rate-limit retry succeeds

When the startup probe hits the authentication-failure 429 limit, handleRateLimited shows this prompt and schedules _authenticateThenStart; if the later probe succeeds, _startProtectedDashboard runs without ever calling _setPromptVisible(false). These new inert assignments therefore leave the sidebar, content, and mobile navigation inaccessible behind the full-screen gate even though authentication succeeded, with a page reload as the only recovery; clear the prompt and inert state on the successful retry path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_startProtectedDashboard now runs HumWatch.auth.closePrompt(), which hides the gate, clears the inert siblings, and cancels any pending retry. Verified live: handleRateLimited then a successful start leaves the gate hidden and all three siblings interactive. Fixed in HEAD.

}
if (visible) {
// A reopened prompt starts clean. handleRateLimited sets its message
// after this call, so the retry notice still shows when it applies.
var error = document.getElementById('auth-error');
if (error) {
error.textContent = '';
error.hidden = true;
}
var input = document.getElementById('auth-token');
if (input) {
input.value = '';
Expand Down
4 changes: 2 additions & 2 deletions static/js/utils/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ HumWatch.theme.STORAGE_KEY = 'humwatch_theme';

HumWatch.theme.available = {
'default': { name: 'Default (Static Hum)', file: null },
'light': { name: 'Light', file: '/css/themes/theme-light.css' },
'terminal': { name: 'Terminal', file: '/css/themes/theme-terminal.css' }
'light': { name: 'Light', file: '/css/themes/theme-light.css?v=2.0.3' },
'terminal': { name: 'Terminal', file: '/css/themes/theme-terminal.css?v=2.0.3' }
};

HumWatch.theme.get = function() {
Expand Down
2 changes: 1 addition & 1 deletion static/vendor/SHA256SUMS
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ b4411fbf2e83a2eecb51ff1b7941b5f2b248ec071adf72c7b4c248fb67c76e13 static/vendor/
# local dashboard stylesheets
0d52de40488d21b1210cc25cf9ab322c589bd0bdc64365a20a3c6c4a522df492 static/css/theme.css
363743fc87c6837c1ee9d062b9778f08a76f202fe68be5676dfe2f8fe7ddb067 static/css/layout.css
369e61fe2f4e53fa83f9cf4f15ad1318e0a760c2db221a4eb1f9c7c98d735e2d static/css/components.css
005be616ec48e00ca1c6a3d8e987c5ed74d73b4ff7de83907e322f9cb2411c2d static/css/components.css
Loading