diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c09392..af643de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,26 @@ skip versions are the composition of each intervening adjacent
release's steps in order — no A-to-B path is pre-computed beyond
that.
+## 0.31.3 — 2026-05-30
+
+**Patch — admin Users tab: "Last seen" reads "Never" for unclaimed
+invites. Visual/logic only in `Admin.jsx`. A plain frontend rebuild
+applies it.**
+
+An admin-created invite row showed a real-looking "Last seen" timestamp
+identical to "Signed up," implying the invitee had visited when they
+hadn't. Cause: `users.last_seen_at` is `NOT NULL DEFAULT (datetime('now'))`
+(`migrations/001_users_and_audit.sql`) and the invite INSERT
+(`invites.py`) sets neither timestamp, so both default to the
+row-creation instant; `last_seen_at` only advances on a real
+authentication. Since an unclaimed invite has provably never
+authenticated (that unclaimed state is exactly what drives the
+"PENDING INVITE" badge), the Users tab now renders **"Never"** for the
+Last-seen cell of a pending-invite row instead of the misleading
+default. Signed-up (the invite-created date) is unchanged.
+
+Upgrade steps: none. **SHOULD** deploy as a normal code deploy.
+
## 0.31.2 — 2026-05-29
**Patch — landing (`/`) welcome panel spacing. Visual only: CSS in
diff --git a/VERSION b/VERSION
index c415e1c..8239f42 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.31.2
+0.31.3
diff --git a/frontend/package.json b/frontend/package.json
index 654316d..d9085ba 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "rfc-app-frontend",
"private": true,
- "version": "0.31.2",
+ "version": "0.31.3",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/components/Admin.jsx b/frontend/src/components/Admin.jsx
index f47ddf7..ca424e2 100644
--- a/frontend/src/components/Admin.jsx
+++ b/frontend/src/components/Admin.jsx
@@ -326,7 +326,14 @@ function UserRow({ user: u, busy, onChangeRole, onToggleMute, onFlipPermission }
)}