feat(profile): pronouns, links & local time on profiles (LC-533) #510

Merged
longjacksonle merged 4 commits from feat/LC-533-profile-extras into main 2026-07-06 02:48:56 +02:00

What

LC-533: richer public profiles. Adds three optional fields a user sets in Settings and that show on their hovercard: pronouns, links, and a timezone that drives a live "local time" line.

Why

The profile card was name + username + bio + status only. Pronouns, a personal link or two, and "what time is it for them right now" are the standard next tier of profile info and cost nothing to add on top of the existing hovercard + settings-profile plumbing.

How

Four commits, each compiles and tests green on its own:

  1. Columns + model plumbing. New migration 0036_profile_extras.sql adds NULLable pronouns, profile_links, timezone to users. Both UserRecord and User carry them, every user SELECT + row_to_user_record map them, and update_user_profile takes them. Model helpers land with lib unit tests (CI-covered): validate_profile_links (per-line http(s) + length + 5-link caps, javascript:/data: rejected before they can reach an href), profile_link_list (defensive render-time re-filter), local_time_in (chrono-tz "3:45 PM EDT", None for unset/unknown tz).

  2. Settings edit. post_profile parses + validates each field (bad value returns the same inline settings_error fragment as the existing display-name/bio checks). The profile section grows a pronouns input, a links textarea (one URL per line), and a timezone <select> reusing the DND picker's IANA list. en + es strings.

  3. Hovercard display. Pronouns render next to the name, the timezone becomes a "Local time: 3:45 PM EDT" line computed at request time, links render as target=_blank rel=noopener noreferrer nofollow anchors. All three sit behind the same privacy gate as bio (public profile, or self, or admin) since the timezone leaks rough location.

  4. Integration tests. update_user_profile round-trip (persists then nulls the extras) and the hovercard privacy gate (public profile shows all three; a stranger on a private profile sees none).

Notes

  • No enforcement/security surface: these are display-only fields. The only hardening is the write-path scheme allowlist + the read-path re-filter keeping non-http(s) URLs out of rendered hrefs.
  • Migration is additive (three ADD COLUMN); no operator action.

Testing

just check, just test, just test-saas all green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu

## What LC-533: richer public profiles. Adds three optional fields a user sets in Settings and that show on their hovercard: **pronouns**, **links**, and a **timezone** that drives a live "local time" line. ## Why The profile card was name + username + bio + status only. Pronouns, a personal link or two, and "what time is it for them right now" are the standard next tier of profile info and cost nothing to add on top of the existing hovercard + settings-profile plumbing. ## How Four commits, each compiles and tests green on its own: 1. **Columns + model plumbing.** New migration `0036_profile_extras.sql` adds NULLable `pronouns`, `profile_links`, `timezone` to `users`. Both `UserRecord` and `User` carry them, every user `SELECT` + `row_to_user_record` map them, and `update_user_profile` takes them. Model helpers land with lib unit tests (CI-covered): `validate_profile_links` (per-line http(s) + length + 5-link caps, `javascript:`/`data:` rejected before they can reach an href), `profile_link_list` (defensive render-time re-filter), `local_time_in` (chrono-tz "3:45 PM EDT", None for unset/unknown tz). 2. **Settings edit.** `post_profile` parses + validates each field (bad value returns the same inline `settings_error` fragment as the existing display-name/bio checks). The profile section grows a pronouns input, a links textarea (one URL per line), and a timezone `<select>` reusing the DND picker's IANA list. en + es strings. 3. **Hovercard display.** Pronouns render next to the name, the timezone becomes a "Local time: 3:45 PM EDT" line computed at request time, links render as `target=_blank rel=noopener noreferrer nofollow` anchors. All three sit behind the **same privacy gate as bio** (public profile, or self, or admin) since the timezone leaks rough location. 4. **Integration tests.** `update_user_profile` round-trip (persists then nulls the extras) and the hovercard privacy gate (public profile shows all three; a stranger on a private profile sees none). ## Notes - No enforcement/security surface: these are display-only fields. The only hardening is the write-path scheme allowlist + the read-path re-filter keeping non-http(s) URLs out of rendered hrefs. - Migration is additive (three `ADD COLUMN`); no operator action. ## Testing `just check`, `just test`, `just test-saas` all green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu
Add three NULLable public-profile fields to `users`: `pronouns`, `profile_links` (newline-separated http(s) URLs), and `timezone` (IANA name, distinct from the DND quiet-hours tz). New migration 0036 plus the read path: both `UserRecord` and `User` carry the fields, every `SELECT` column list and `row_to_user_record` map them, and `update_user_profile` takes them (the settings handler still passes None until the next commit wires the form).

Model helpers land here with lib unit tests so the validation/formatting logic is CI-covered: `validate_profile_links` (per-line http(s) + length + count caps, javascript:/data: rejected before it can reach an href), `profile_link_list` (defensive render-time re-filter), and `local_time_in` (chrono-tz formatted "3:45 PM EDT", None for unset/unknown tz so the line simply hides).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu
Wire the write path and the settings-form UI for the three profile extras. `post_profile` now parses and validates each multipart field: pronouns cap at 40 chars, links through `validate_profile_links` (per-line http(s) + length + 5-link count caps), timezone against `chrono_tz::Tz`; a bad value returns the same inline `settings_error` fragment as the existing display-name/bio/email checks rather than silently dropping. The profile section grows a pronouns input, a links textarea (one URL per line), and a timezone `<select>` that reuses the DND picker's IANA list with a dedicated `profile_timezone` pre-select. en + es strings added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu
Surface the three extras on the profile popover. Pronouns render de-emphasised next to the display name, the timezone becomes a live "Local time: 3:45 PM EDT" line computed at request time, and links render as external anchors (target=_blank, rel=noopener noreferrer nofollow) with the http(s) scheme re-checked on read. All three sit behind the same privacy gate as bio (public profile, or self, or admin) since the timezone in particular leaks rough location. en + es label added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu
test(profile): profile-extras round-trip + hovercard privacy gate (LC-533)
All checks were successful
check-secrets / Nosey parker (push) Successful in 9s
check-secrets / TruffleHog (push) Successful in 13s
check-secrets / Kingfisher (push) Successful in 14s
check-secrets / Nosey parker (pull_request) Successful in 4s
check-secrets / Kingfisher (pull_request) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 6s
Check / clippy + fmt + tests (pull_request) Successful in 4m12s
Create release / Create release from merged PR (pull_request) Has been skipped
ff965166ec
Cover the two integration surfaces the lib unit tests can't: `update_user_profile` persists pronouns/links/timezone and nulls them on clear (db_auth), and the hovercard shows all three for a public profile (pronouns, link href, "PM EDT" local time) while a stranger viewing a private profile sees none of them (db_auth caller updated to the new 7-arg signature).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-07-06 02:46:26 +02:00
longjacksonle deleted branch feat/LC-533-profile-extras 2026-07-06 02:48:56 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!510
No description provided.