VPN connection log: who connected and when

WireGuard has no connect events, only the last handshake and byte counters.
awg-ctrl now checks them every 30 s and writes conn_log in users.db: connect
at the first handshake, disconnect at the last activity (the last tick the
counters moved), with the session length and traffic, plus peer_seen, the
last time each peer was online. Open sessions are restored from the database,
so a restart adds no duplicates; a down interface is not a disconnect; a peer
removed mid-session is closed. Events keep the name, owner and VPN IP as of
that moment and live 90 days. Client public IPs are not recorded.

GET /api/connlog serves the events and the per-peer last-seen list. The panel
fetches it from the selected server at /ui/connlog and fills in master key
labels and device names for sen:// peers. A new "Подключения" tab shows the
event feed with search and paging, and "who was online when", including
those who never connected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
This commit is contained in:
Claude
2026-10-28 12:06:40 +00:00
parent fd5e2cc761
commit 1a54d05cca
13 changed files with 802 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
/* Вкладка «Подключения»: лента событий и список «кто когда был». Строки — гибкие, без таблицы:
на телефоне колонки сами переносятся. */
.cl { display: flex; flex-direction: column; gap: 12px; }
.cl-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.cl-views { display: flex; gap: 8px; }
.cl-view {
min-height: 44px; padding: 0 18px;
font: inherit; font-size: 14px; color: var(--on-surface);
background: var(--surface-bright);
border: 1px solid var(--outline); border-radius: var(--r-pill);
cursor: pointer;
}
.cl-view:hover { background: var(--state-primary); }
.cl-view--on { background: var(--primary-container); color: var(--on-primary-container); border-color: transparent; font-weight: 500; }
.cl-search { flex: 1; min-width: 200px; width: auto; }
.cl-hint { margin: 0; font-size: 13px; color: var(--neutral-text); }
.cl-err { text-align: left; }
.cl-list {
list-style: none; margin: 0; padding: 0;
background: var(--surface-bright);
border: 1px solid var(--outline-variant); border-radius: var(--r-card);
overflow: hidden;
}
.cl-row {
display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px;
padding: 12px 16px;
border-top: 1px solid var(--outline-variant);
}
.cl-row:first-child { border-top: none; }
.cl-time { flex: 0 0 110px; font-size: 13px; color: var(--neutral-text); font-variant-numeric: tabular-nums; }
.cl-chip { flex: none; }
.cl-who { flex: 1 1 200px; display: flex; flex-direction: column; min-width: 0; }
.cl-who b { font-weight: 500; overflow-wrap: anywhere; }
.cl-sub { font-size: 12px; color: var(--neutral-text); overflow-wrap: anywhere; }
.cl-session, .cl-seen { font-size: 13px; color: var(--on-surface); font-variant-numeric: tabular-nums; }
.cl-empty { padding: 24px 16px; text-align: center; color: var(--neutral-text); }
.cl-more { display: flex; justify-content: center; padding: 12px; border-top: 1px solid var(--outline-variant); }
@media (max-width: 640px) {
.cl-time { flex-basis: auto; }
.cl-views { width: 100%; }
.cl-view { flex: 1; }
}