The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
The P1 epic: the upload route reads its own body after `require_human`
resolves, so an anonymous caller can no longer make the server spool 500 MiB
before the auth check runs. Three deep-review fix rounds on top of it.
E16: a recorded debug session becomes a drafted issue with its own logs attached, sent to the private Gitea tracker or downloaded as a zip. Includes the four epic-end review rounds and the Plex player's focus trap.
E22 (activity trail): a consented, self-expiring diagnostics level with the user's own log visible
before anything is sent, last sign-in, bilingual privacy policy and terms, an admin diagnostics
section, and pickers for every closed-set setting. Plus what the epic's measurements turned up on
the way: doubled chat delivery, Settings on a phone, locale-ignoring dates and numbers, and the
feed poll narrowed to followed channels.
Back, dialogs and menus keep your place, and the keyboard walks every list: five sprints plus three epic-end review rounds (19 → 14, 18 → 12, 14 → 14). Minor: j/k list navigation, s and g-chords, and Back-to-item are features.
Sixteen items written from real use: eleven fixed, one measured and closed without a code change
(the micro-stutter is a 23.976 fps film on a 120.0148 Hz display, reproduced identically outside
Siftlode), two parked for a reproducible case, one scoped out to its own sprint. Two of the eleven
were destroying user state — a YouTube video that rewound itself on reopen, and a "clear resume
point" that told Plex nothing about an in-progress item.
Then seven deep-review rounds over the fixes and over each other's fixes: 32 → 24 → 43 → 31 → 22 →
26 → 18 raised. Minor, not patch: item 14 added an admin knob, item 10 a sort control, and items
2/4/5/7 are features.
Release notes for the epic: bilingual mail, one-mail-one-click approval, order-independent gates
with named states, canonical Gmail identity, the admin lifeline for sign-ups stuck outside, and the
four-round closing review that converged 12 -> 4 -> 3 -> 2.
E9 Playlist module: total playtime in both playlist headers, the header
compacted to two rows, and the sticky column header sitting flush. Plus the
six defects the epic-end deep review confirmed.
Round 2 on the feature diff (15 → 12):
- still_allowed_by_env now rides the invite LIST as well as the delete response,
so the confirmation warns while the admin can still change their mind. It
mattered most for a DENIED row on an env-allow-listed address: deleting it
silently converted a refusal into a grant, and the only warning arrived after
the irreversible click.
- Both inbox bodies claimed to name "the most recent" dropped message. The
throttle records the FIRST of each hour and never updates it, so they now say
"one of them", and the not-configured body finally names where to fix it.
- settings.invites.emailOff told the admin they "get no notice when someone
signs up" — rendered directly above the queue that shows exactly that. Only
the email is lost, and it now says so.
- The disabled delete buttons put their reason out of reach: `disabled` drops a
control from the tab order and suppresses its title. Switched to
aria-disabled + preventDefault, the pattern Playlists.tsx already established
for the same reason.
- deleteInvite is quiet, so the 409 the UI handles with a translated toast no
longer also raises the server's English detail as a modal.
- After a completed password reset, Back landed on the reset form again with a
consumed token — the history change made that state reachable. The token is
cleared on success so the reset view stops being the card's root.
- The hu deleteHasAccount named a tab that renders under a different label.
Tests: a dom test for the Access-requests panel covering both has_account
variants and all three delete outcomes, /api/me flag cases, an ADMIN_EMAILS
case, and the env-allowlist test now patches the raw setting so the parsing
stays under test.
Round 1 on the feature diff found 15; the substantive ones:
- The delete control VANISHED on any invite whose address had registered, which
is the ordinary sign-up shape (register writes the User and the pending invite
together). The guard stays broad — that row is the account's approval gate, and
orphaning it strands someone who can then neither sign in nor be approved — but
the control is now a disabled button carrying the reason, on both lists, so an
admin never sees a row silently missing a button its neighbours have.
- Deleting an entry is not always a revoke: is_allowed falls back to the env
ALLOWED_EMAILS / ADMIN_EMAILS lists once no row exists, and an ADMIN_EMAILS
address is force-promoted on its next Google sign-in. The route now reports
still_allowed_by_env and the UI says so instead of claiming access was removed.
- "Deny keeps the row so they can't immediately re-ask" was simply false —
upsert_pending_invite reopens a denied row. Corrected everywhere it was stated.
- The Configuration warning reads me.email_configured, which save() did not
invalidate, so it kept warning about fields the admin had just filled in.
- The read-only variant was an aria-hidden span explained only by a Tooltip that
the hints toggle can switch off; now a disabled button with title + aria-label.
- The delete error path threw away the 409's reason (the stale-list case) and
left the stale row on screen.
- The throttle window was spent even when no notice was written, silencing the
next hour on the path whose job is to stop silent drops.
- email_enabled() opened a second session and decrypted the SMTP password on
every /api/me poll; it now takes the caller's session.
- welcomeMode.dom.test.ts never rendered the card, so reverting the history
wiring to useState left it green. A render test now drives the real links and
fails on that revert (verified).
Plus /api/me flag tests, the registration-shaped pending case, the env-allowlist
case, and a release note that matches what the code does.
Three operator gaps, all found the same way — a person could not sign up and the
app showed nobody anything.
- DELETE /api/admin/invites/{id} with a delete control on both the pending
requests and the previously-decided list. Deny keeps the row so the person
cannot immediately re-ask; delete forgets the address. Refused with 409 while
a User row exists for it, because is_allowed reads that row and removing it
would revoke a live user's access with no other sign — the list ships
has_account so the UI withholds the button instead of offering one that fails.
- Email is fail-soft by design, which had come to mean invisible. _send() now
records an email_undelivered notification for every admin when a message is
skipped or rejected, naming the message that was lost, throttled to one an
hour. /api/me carries email_configured, driving a standing warning on
Configuration → Email / SMTP and above the access-request list.
- The sign-in card's Create account / Forgot password / Didn't get the email
views are history entries (useHistorySubview), so Back returns to the previous
view instead of only moving the address bar.
Also: the per-test rate-limiter reset sweeps every loaded app.* module rather
than a hand-kept list of three — this change added a limiter outside it.
Round 6's two remaining nits. The operator note said "Configuration → Email"
but the group renders as "Email / SMTP" in both locales, and
test_an_admin_can_activate_a_pre_approved_registrant requested make_user without
using it — a leftover from the reverted rounds that made the test look like it
seeded an admin when it did not.
Round 5's correctness/security pass came back clean; what was left was text that
said more than the code does.
- The register-path notify comment (and its test) claimed the admin "is still
told". The call fires, but on a no-SMTP instance _send drops it — which is the
open half of the problem, not something this branch fixed. Both now say so.
- The release notes tell admins what to actually do: an account whose address was
whitelisted in advance has no pending request to approve, so the whitelist box
is what activates it. That instruction only existed in the roadmap and in
chores, which the dialog does not render.
- "That one line is the whole production change" now accounts for the test-only
RateLimiter.reset() that also ships.
- The rate-limiter test claimed to pin that blocked attempts are not counted, on
an assertion that held either way. Replaced with one that drives the clock
across the window boundary and fails if hammering extends it.
Round 4 showed the `inv is not None` gate added in b5a1002 removed a signal that
matters: for an address the admin had already approved, that email is their only
notice that someone has now created an account on it — the invite is not pending
so nothing lands in the Access-requests queue either. Suppressing it left the
registrant at "awaiting admin approval" with the admin unaware they existed. The
duplicate it avoided (Request access, then sign up) is mild noise by comparison.
The production change is now one db.commit(), which is all the reported bug ever
needed. Release notes corrected again: a previously-denied address was also
unaffected, the pending-request case never lost its queue entry (only the
account behind it), and the test inventory was missing a guard.
Round 3 of the review killed the reopen from b5a1002: flipping an approved
invite back to pending let an unauthenticated POST /auth/register REVOKE a
standing approval for any whitelisted-but-unclaimed address, which then fails
is_allowed() and locks the real owner out of Google sign-in — the primary way
people get in here. It also re-armed the pre-emption window it was meant to
close, because with no SMTP email_verified is set True and the SEC-10 guard the
design leaned on never fires.
Two attempts at fixing the stranded-account UX in the same change both came back
worse than the wart. What remains is the narrow, real bug: the account was
silently rolled back and is now committed. An admin still binds the approval to
the account, exactly as before this branch. UX-25 in the roadmap records the
follow-up and both dead ends so they are not retried.
Also from round 3: the upsert docstring no longer claims the admin UI cannot
reach these accounts (re-entering the address in the same panel's whitelist box
does, and a test now pins it), the release notes are scoped to what the diff
actually changes with the operator note moved where the dialog renders it, and
RateLimiter has unit tests so reset() is not production code only a fixture
calls.
Replaces the activation added in ea931dc, which round 2 of the review showed was
a real weakening: on a no-SMTP instance it made a standing whitelist entry a
bearer grant — the first anonymous party to register an approved-but-unclaimed
address got an immediately usable account under that identity, with no
attestation, no admin action and no signal anywhere. The verify-time activation
had the same shape one step removed, and neither site ran the SEC-10 preset-
password guard or left an audit trail.
Keep a human in the loop instead. An approval granted before any account existed
was a decision about the ADDRESS, so registration reopens the invite
(upsert_pending_invite gains revive_approved, used only by the register worker).
The request lands in the Access-requests queue, approving it runs
_activate_user_by_email — and with it revoke_preset_password_if_unverified — and
the account activates. One mechanism, already audited, no new grant path, and
the "awaiting admin approval" copy is honest again.
The explicit commit from the previous commit still matters: with an invite
already pending, upsert returns without committing and _issue_token is gated on
SMTP, so a no-SMTP registration was still being rolled back.
Also from round 2: gate the admin notice on a fresh decision (matching the two
other upsert callers) so a retry no longer re-pings every admin, and reset the
module-level rate limiters between tests — the suite pooled every login into one
300s window and was two logins short of failing an unrelated test with a 429.
Follow-up to the commit fix, from the deep-review. Persisting the account was
only half the story: when the admin had approved the address BEFORE the person
registered, the account landed is_active=False against an invite that is no
longer pending — absent from the Access-requests queue, and approve_invite 409s
on an already-decided invite. The sign-up worked and still could not sign in.
Derive activation from is_allowed() at the moment ownership is attested:
- verify_email_confirm activates a pre-approved account when the emailed link is
confirmed (only the mailbox owner can follow it), so SEC-10 stays intact;
- _register_account activates only when there is no SMTP to verify with, where
the admin whitelist is by design the sole gate — the same one add_invite
already activates on.
Tests: the pre-approved no-SMTP case and the verify-activates case are real
regression tests (both fail without this change); guards cover the unknown-email
and denied->pending branches, verification of an unapproved account, and the
first end-to-end coverage of the /auth/register route and its
allow_registration gate. Release notes rewritten — the previous entry miscounted
its regression tests and advertised an email fix this release has no code for.
_register_account() flushed the new User but relied on someone else to commit:
upsert_pending_invite() commits only when it actually writes an invite row, and
_issue_token() runs only when SMTP is enabled. For an email an admin had ALREADY
approved, on an instance where email is unavailable, neither fired — the session
closed and the account was rolled back. The would-be user got the uniform "ok"
and no account, could not sign in, and re-registering was a silent no-op.
Commit explicitly after the invite upsert (a no-op when upsert already committed
the same transaction). Three DB-lane regression tests cover the pre-approved
no-SMTP path (fails without the fix), the unknown-email path that always worked,
and the pre-approved path with SMTP on.
hls.js startPosition:0 so the transcode/remux HLS session starts VOD-style
at the requested position instead of chasing the live edge of the growing
EVENT playlist.
Bundles the whole E7 epic (library metadata, crop fix, share-by-default, in-app player,
browser-playable guarantee, local upload) plus the deep-review hardening. Bump VERSION and
add the 0.76.0 release-notes entry (RELEASE_NOTES[0] must match VERSION).
Fixes from several multi-agent deep-review passes over the E8 feed-filtering &
channel-manager epic (S1-S4):
- feed_years buckets by UTC year to match the UTC-pinned year-filter bounds
- tag suggestions: examples ∩ subscribed (drop tagged-then-unsubscribed channels
from the profile denominator); exclude hidden channels as suggestion targets
- channel manager: restore ghost chips + surface an error toast on a failed tag accept
- year filter: debounce the search term so the per-year facet doesn't refetch per keystroke
- channel tags: aria-label the "+N" overflow pills
- extract useTagSuggestions hook; add backend + hook tests (profile boundaries, cap,
language exclusion, unsubscribed/hidden handling, UTC bucketing, bulk-accept)
Bump VERSION to 0.73.0 + release notes.
Bump VERSION to 0.70.0 and add the user-facing release-notes entry for the E4
Frontend Platform epic: full bookmarkable/shareable address-bar URLs (retiring the
"Share view" button), touch-visible card action buttons with a shape (not
colour-only) watched marker, and the bookmark-page-closes-on-modal-close fix — over
a large internal front-end refactor (god-component splits, usePref, useUrlMirror,
FilterRail, Modal focus). No DB migration.
Three prod bug fixes: local-poster-primary Library thumbnails, viewport-scaled
feed read-ahead (fixes the fast-scroll "Load more" stall), and the channel About
popover fitting/blank-line cleanup.