feat: legacy credentials importer + two-column /login layout#122
Merged
Conversation
Adds the missing private-side cutover tool. Reads a CSV exported from laddr's MySQL (Username,Email,Password) and a bare clone of the codeforphilly-data published branch, joins by slug, and emits profiles.jsonl + legacy-passwords.jsonl ready for upload to the runtime private store (PVC for sandbox, S3-compat bucket including GCS for prod). The public import-laddr script deliberately doesn't touch private data because laddr's public JSON API doesn't expose emails or password hashes; this new script consumes a MySQL extract directly, which fills the gap. Full-replace semantics: each run overwrites the local output files. Cutover-time use only — re-running after in-app rehash-on-login would clobber argon2id rotations with the SHA-1 originals. Schema validation routes invalid emails / malformed hashes through the same Zod schemas the runtime uses, so the output is guaranteed to load cleanly into the in-memory private store. Reports counts and warnings; defaults to producing files under .scratch/ so PII stays out of git. Operator doc at docs/operations/legacy-credentials-import.md covers the CSV format, run command, deploy paths for both sandbox (kubectl cp + rollout restart, since reload-data only covers public state) and prod (gsutil/aws s3 cp + restart). Linked from cutover.md's T-1 section, which previously incorrectly steered operators toward the deferred account-claim flow for credentials. .gitignore picks up .claude/scheduled_tasks.lock (per-session ScheduleWakeup artifact, never want it tracked). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the single-card layout (GitHub button primary, password form hidden behind a disclosure) with a two-column grid: password on the left, GitHub on the right, both fully expanded. Cutover-time users arriving at /login are now overwhelmingly legacy users; the disclosure made the path they need invisible by default. Layout adjusts to max-w-5xl with a centered header; columns stack on mobile via grid-cols-1 md:grid-cols-2. Forgot-password link stays on the password card. WhyGitHub explainer moves under the GitHub card since it's the long-form explainer for that column. Test updates: drops the disclosure-expansion tests (no longer applicable); adds an "both columns visible" assertion as the entry- point check. Other tests (gated submit, inline 401 error) stay as-is since the field selectors haven't changed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legacy password sign-in flow called navigate() after a successful POST /api/auth/login but never reloaded the AuthProvider's snapshot, so the navbar (and every other useAuth consumer) stayed in its anonymous state until the next hard refresh. The session cookies were set on the server, the user was technically signed in, the UI just hadn't noticed. GitHub OAuth doesn't have this bug because the flow is a full-page navigation through /api/auth/github/start → callback → return path, which re-mounts AuthProvider and re-fetches /api/auth/me on its own. The password-reset confirm flow already awaits reload() before navigating; the legacy login path had drifted from that pattern. Same fix: await reload() in LoginPlaceholder's onSuccess before navigating. Widens the onSuccess type to allow a Promise return. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related changes uncovered while debugging "my login for chris isn't working" on sandbox.
1. Credentials importer (
feat(scripts): import-laddr-credentials)The login-migration impl track (phases A–D, PRs #118–#121) shipped runtime support for password sign-in, but there was no tool to get the passwords there. The existing `script:import-laddr` deliberately handles only public data (laddr's public JSON API doesn't expose emails or password hashes). This PR adds the private-side counterpart.
Sanity-checked on the real 31,869-row laddr export: 30,360 written (95% match rate), 869 skipped no-email, 640 no-person-match. Deployed to sandbox and verified `chris`'s sign-in works end-to-end against his bcrypt hash.
New operator doc: docs/operations/legacy-credentials-import.md — CSV format, run command, deploy paths for both sandbox (`kubectl cp` + rollout restart) and prod (`gsutil cp` / `aws s3 cp` + restart). Note the deploy nuance: `POST /api/_internal/reload-data` only reloads public + FTS, so private-store updates require a full pod restart.
`cutover.md` updated — its T-1 section was incorrectly steering operators to the deferred account-claim flow for credentials; now links to the credentials-import doc.
2. Two-column /login layout (
feat(web): two-column /login)Cutover-time users arriving at `/login` are overwhelmingly legacy users; the previous "GitHub button primary, password form hidden behind a disclosure" layout made the path most users actually need invisible by default.
Test plan
🤖 Generated with Claude Code