Fix: Recover proxied client IPs and fit audit filters on phones

The MFC web server now resolves the real client address from the
WebSocket upgrade request when the TCP peer is loopback, so audit records
and login rate limiting stop reporting the reverse proxy's 127.0.0.1.
X-Forwarded-For is preferred over X-Real-IP because its last hop is the
address the local proxy appended; a client-supplied header list cannot
forge it. Forwarded headers are ignored for non-loopback peers so a
client connecting directly over the internet cannot spoof its identity
or rotate addresses to bypass throttling. FRP deployments can keep using
the existing Proxy Protocol v2 support instead.

Collapse the audit log search conditions behind a toolbar toggle that
starts closed and switches to an accent-colored "Hide filters" state
while the panel is open. On portrait phones the panel uses one control
per row, and From/To move their border and padding onto a wrapper so the
iOS WebKit datetime-local width miscalculation cannot overflow the
viewport.

Co-Authored-By: deepseek-flash
This commit is contained in:
yuanyuanxianganddeepseek-flash committed 2026-10-19 10:05:58 +02:00
1 parent ca671d6453
commit bbb3d8c2fa
2 files changed
+156 -5

No files matched your search

+80 -5
View File
@@ -391,6 +391,10 @@
padding: 16px;
margin-bottom: 14px;
}
.audit-filters.collapsed { display: none; }
/* Filter labels are grid items; without this a wide intrinsic child
(notably datetime-local on mobile) can blow out the grid track. */
.audit-filters > * { min-width: 0; }
.audit-filters label {
display: flex;
flex-direction: column;
@@ -409,8 +413,37 @@
font-size: 13px;
outline: none;
min-width: 0;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.audit-filters input:focus, .audit-filters select:focus { border-color: #e67e22; }
/* iOS Safari mis-sizes temporal controls: with horizontal padding on
the input itself, width:100% adds the padding again (WebKit #301648),
and iOS 17 ignores width entirely on the native inline-flex control.
Move the visual box to a wrapper and keep the input padding-free so
it can never exceed its parent. */
.audit-date-wrap {
display: block;
min-width: 0;
padding: 0 10px;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 6px;
background: rgba(15, 52, 96, 0.7);
}
.audit-date-wrap:focus-within { border-color: #e67e22; }
.audit-date-wrap input[type="datetime-local"] {
display: block;
width: 100%;
max-width: 100%;
min-width: 0;
padding: 8px 0;
border: none;
border-radius: 0;
background: transparent;
color: #fff;
font-size: 13px;
}
.audit-filters label.audit-warn {
flex-direction: row;
align-items: center;
@@ -430,6 +463,22 @@
.audit-search-btn:hover { transform: translateY(-1px); }
.audit-reset-btn { background: rgba(255,255,255,0.08); color: #ccc; }
.audit-reset-btn:hover { background: rgba(255,255,255,0.15); }
/* Filters visibility toggle: collapsed by default; the same button
stays in the toolbar and flips to an "open" style (accent tint +
caret pointing up) so it reads as "click to collapse". */
.view-btn.audit-filter-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid rgba(230, 126, 34, 0.45);
border-radius: 8px;
color: #e67e22;
background: rgba(230, 126, 34, 0.12);
}
.view-btn.audit-filter-toggle:hover { background: rgba(230, 126, 34, 0.22); color: #fff; }
.view-btn.audit-filter-toggle.open { background: rgba(230, 126, 34, 0.30); color: #fff; }
.audit-filter-toggle .caret { display: inline-block; font-size: 10px; transition: transform 0.2s; }
.audit-filter-toggle.open .caret { transform: rotate(180deg); }
.audit-status { color: #888; font-size: 12px; margin-bottom: 10px; min-height: 16px; }
.audit-status.warn { color: #e67e22; }
.audit-table-wrap {
@@ -470,8 +519,11 @@
.audit-level.warn { background: rgba(230, 126, 34, 0.18); color: #e67e22; }
.audit-empty { padding: 40px; text-align: center; color: #666; }
@media (max-width: 640px) {
.audit-filters { grid-template-columns: 1fr 1fr; }
.audit-actions { grid-column: 1 / -1; }
/* One control per row: two columns plus the intrinsic width of
datetime-local inputs overflows a portrait phone viewport. */
.audit-filters { grid-template-columns: minmax(0, 1fr); padding: 14px; }
.audit-actions { grid-column: 1 / -1; flex-wrap: wrap; }
.audit-actions button { flex: 1 1 auto; }
.audit-table td, .audit-table th { padding: 8px; }
}
@@ -1787,15 +1839,24 @@
<h2>Audit Log <span id="audit-count" class="device-count">0</span></h2>
</div>
<div class="toolbar">
<button class="view-btn audit-filter-toggle" id="audit-filter-toggle" type="button"
aria-expanded="false" aria-controls="audit-filters"
onclick="toggleAuditFilters()">
<span class="label">Filters</span><span class="caret">&#9662;</span>
</button>
<button class="view-btn" onclick="queryAudit(0)" title="Reload newest records">Refresh</button>
</div>
</div>
<div class="audit-filters">
<div class="audit-filters collapsed" id="audit-filters">
<label>From
<input type="datetime-local" id="audit-from" onkeydown="if(event.key==='Enter')queryAudit(0)">
<span class="audit-date-wrap">
<input type="datetime-local" id="audit-from" onkeydown="if(event.key==='Enter')queryAudit(0)">
</span>
</label>
<label>To
<input type="datetime-local" id="audit-to" onkeydown="if(event.key==='Enter')queryAudit(0)">
<span class="audit-date-wrap">
<input type="datetime-local" id="audit-to" onkeydown="if(event.key==='Enter')queryAudit(0)">
</span>
</label>
<label>Event (exact or prefix)
<input type="text" id="audit-event" list="audit-event-options" placeholder="e.g. web.* or web.login.failure"
@@ -4542,6 +4603,20 @@
if (auditBtn) auditBtn.classList.toggle('visible', admin && !!serverCaps.auditLog);
}
// Search conditions start collapsed (especially important on portrait
// phones); the toolbar button stays put and switches to a highlighted
// "up" state while the panel is open, meaning clicking again folds it.
function toggleAuditFilters() {
const panel = document.getElementById('audit-filters');
const btn = document.getElementById('audit-filter-toggle');
if (!panel || !btn) return;
const collapsed = panel.classList.toggle('collapsed');
btn.classList.toggle('open', !collapsed);
btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
const label = btn.querySelector('.label');
if (label) label.textContent = collapsed ? 'Filters' : 'Hide filters';
}
function openAuditPage() {
if (currentUserRole !== 'admin' || !serverCaps.auditLog) return;
fillAuditEventOptions();