Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e595b6e5e | |||
| 1d716d0cb8 |
@@ -23,6 +23,44 @@ skip versions are the composition of each intervening adjacent
|
|||||||
release's steps in order — no A-to-B path is pre-computed beyond
|
release's steps in order — no A-to-B path is pre-computed beyond
|
||||||
that.
|
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
|
||||||
|
`App.css` (`.welcome`). A plain frontend rebuild applies it.**
|
||||||
|
|
||||||
|
The welcome read-view was jammed against the catalog divider with no
|
||||||
|
top offset and loose, uneven paragraph spacing. Root cause: `.main-pane`
|
||||||
|
carries a bare `.main-pane { padding: 0; display: flex }` override (the
|
||||||
|
§8 three-column RFC shell) that shadows the earlier padded read-view
|
||||||
|
rule, so the pane provides no padding — and `.welcome` (just
|
||||||
|
`max-width`) never compensated. The welcome surface now owns its own
|
||||||
|
breathing room: 56px top / 48px side gutters, a capped 680px measure,
|
||||||
|
a stronger `text-3xl` "Welcome." hero, and even `--space-8` paragraph
|
||||||
|
rhythm at `--leading-relaxed`. Applies to both the signed-out and
|
||||||
|
signed-in welcome (same `.welcome` class).
|
||||||
|
|
||||||
|
Upgrade steps: none. **SHOULD** deploy as a normal code deploy.
|
||||||
|
|
||||||
## 0.31.1 — 2026-05-29
|
## 0.31.1 — 2026-05-29
|
||||||
|
|
||||||
**Patch — admin Users tab + header UX polish. Visual only: CSS plus
|
**Patch — admin Users tab + header UX polish. Visual only: CSS plus
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "rfc-app-frontend",
|
"name": "rfc-app-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.31.1",
|
"version": "0.31.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+21
-3
@@ -189,9 +189,27 @@
|
|||||||
padding: 32px 48px;
|
padding: 32px 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome { max-width: 640px; }
|
/* `.main-pane` is the §8 flex shell and carries no padding (the bare
|
||||||
.welcome h1 { font-size: var(--text-2xl); font-weight: 600; margin: 0 0 16px; }
|
`.main-pane` override below shadows the padded read-view rule), so the
|
||||||
.welcome p { line-height: 1.7; color: var(--c-gray-600); }
|
welcome surface owns its own breathing room — top offset, comfortable
|
||||||
|
side gutters, and a capped measure for readable line length. */
|
||||||
|
.welcome {
|
||||||
|
max-width: 680px;
|
||||||
|
padding: 56px 48px 64px;
|
||||||
|
}
|
||||||
|
.welcome h1 {
|
||||||
|
font-size: var(--text-3xl); font-weight: 600;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
margin: 0 0 var(--space-8);
|
||||||
|
}
|
||||||
|
.welcome p {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
color: var(--c-gray-600);
|
||||||
|
margin: 0 0 var(--space-8);
|
||||||
|
}
|
||||||
|
.welcome p:last-child { margin-bottom: 0; }
|
||||||
|
.welcome strong { color: var(--c-gray-800); }
|
||||||
|
|
||||||
/* --- RFC / Proposal view (read-only for slice 1) --- */
|
/* --- RFC / Proposal view (read-only for slice 1) --- */
|
||||||
|
|
||||||
|
|||||||
@@ -326,7 +326,14 @@ function UserRow({ user: u, busy, onChangeRole, onToggleMute, onFlipPermission }
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<TimeCell value={u.created_at} />
|
<TimeCell value={u.created_at} />
|
||||||
<TimeCell value={u.last_seen_at} />
|
{/* An unclaimed admin invite has provably never authenticated, so
|
||||||
|
last_seen_at is just the row-creation default (it equals
|
||||||
|
created_at). Render the truth — "Never" — rather than a
|
||||||
|
timestamp that reads like a real visit. */}
|
||||||
|
<TimeCell
|
||||||
|
value={pendingInvite ? null : u.last_seen_at}
|
||||||
|
emptyLabel={pendingInvite ? 'Never' : '—'}
|
||||||
|
/>
|
||||||
</tr>
|
</tr>
|
||||||
{state === 'pending' && u.beta_request_reason ? (
|
{state === 'pending' && u.beta_request_reason ? (
|
||||||
<tr className="user-row-reason">
|
<tr className="user-row-reason">
|
||||||
@@ -345,8 +352,8 @@ function UserRow({ user: u, busy, onChangeRole, onToggleMute, onFlipPermission }
|
|||||||
// Render a "YYYY-MM-DD HH:MM:SS" timestamp as an intentional date-over-time
|
// Render a "YYYY-MM-DD HH:MM:SS" timestamp as an intentional date-over-time
|
||||||
// stack (date prominent, time quiet below) rather than letting a narrow
|
// stack (date prominent, time quiet below) rather than letting a narrow
|
||||||
// column wrap the value mid-string. Falls back to an em-dash when absent.
|
// column wrap the value mid-string. Falls back to an em-dash when absent.
|
||||||
function TimeCell({ value }) {
|
function TimeCell({ value, emptyLabel = '—' }) {
|
||||||
if (!value) return <td className="muted">—</td>
|
if (!value) return <td className="muted">{emptyLabel}</td>
|
||||||
const [date, ...rest] = String(value).split(' ')
|
const [date, ...rest] = String(value).split(' ')
|
||||||
const time = rest.join(' ')
|
const time = rest.join(' ')
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user