chore(comments): drop epic, sprint and review-round ids from the code
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.
This commit is contained in:
@@ -31,11 +31,11 @@ export interface Me {
|
||||
/** Admin-only: false when SMTP is incomplete, so every outbound message is silently skipped. */
|
||||
email_configured: boolean;
|
||||
preferences: Record<string, unknown>;
|
||||
/** The consented diagnostics level in force and when it lapses (E22 S5). */
|
||||
/** The consented diagnostics level in force and when it lapses. */
|
||||
diagnostics: DiagnosticsState;
|
||||
/** The debug session recording right now, if any (E16) — a reload resumes it. */
|
||||
/** The debug session recording right now, if any — a reload resumes it. */
|
||||
debug_session: DebugSession | null;
|
||||
/** Where a bug report goes — the tracker's repository — or null: download only (E16 S4). */
|
||||
/** Where a bug report goes — the tracker's repository — or null: download only. */
|
||||
issues: { repo: string } | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export interface ConfigItem {
|
||||
}
|
||||
export interface SystemConfigData {
|
||||
groups: Record<string, ConfigItem[]>;
|
||||
/** The page's headings, in order, each with its groups in order; every group is in one (E31). */
|
||||
/** The page's headings, in order, each with its groups in order; every group is in one. */
|
||||
sections: { id: string; groups: string[] }[];
|
||||
secrets_manageable: boolean;
|
||||
}
|
||||
@@ -74,14 +74,14 @@ export interface AdminUserRow {
|
||||
* account) from "we took it away" — different problems needing different help. */
|
||||
password_revoked_at: string | null;
|
||||
has_google: boolean;
|
||||
/** The Google account's address, ONLY when it differs from the account address (E9.5 S3). */
|
||||
/** The Google account's address, ONLY when it differs from the account address. */
|
||||
google_email: string | null;
|
||||
created_at: string | null;
|
||||
/** Last sign-in (Google or password). Null = not since 0.88 (no backfill). */
|
||||
last_login_at: string | null;
|
||||
}
|
||||
|
||||
/** The account panel's view of why someone can or cannot get in (E12 S5). */
|
||||
/** The account panel's view of why someone can or cannot get in. */
|
||||
export interface AdminUserAccess extends AdminUserRow {
|
||||
/** The address passes the invite gate — so Google sign-in will be let through, not filed as a
|
||||
* fresh access request. */
|
||||
@@ -193,7 +193,7 @@ export interface SchedulerJob {
|
||||
last_started: string | null;
|
||||
last_finished: string | null;
|
||||
last_result: string | null; // the English one-liner — the fallback for a job with no `result`
|
||||
// The last result as data (E30): counts, a `skipped` / `reason` word, a nested dict per phase —
|
||||
// The last result as data: counts, a `skipped` / `reason` word, a nested dict per phase —
|
||||
// named in the reader's language by lib/jobResult.
|
||||
result: JobResult | null;
|
||||
last_error: string | null;
|
||||
@@ -241,7 +241,7 @@ export const adminApi = {
|
||||
req(`/api/admin/config/${key}`, { method: "DELETE" }),
|
||||
testEmail: (): Promise<{ sent: boolean; to: string }> =>
|
||||
req("/api/admin/config/test-email", { method: "POST" }),
|
||||
/** The bug-report tracker (E16): answers, takes issues, and is private. */
|
||||
/** The bug-report tracker: answers, takes issues, and is private. */
|
||||
testIssues: (): Promise<{ ok: true; repo: string; url: string | null }> =>
|
||||
req("/api/admin/config/test-issues", { method: "POST" }),
|
||||
// --- users & roles ---
|
||||
|
||||
@@ -6,7 +6,7 @@ import { req, uiLang } from "./core";
|
||||
/** Why password sign-in refused. The three account-shaped reasons name the mailbox and both gates,
|
||||
* so the card can render an honest state instead of a red line; `invalid` and `rate_limited` answer
|
||||
* BEFORE the password is proven and therefore carry nothing (they must stay identical for a real
|
||||
* account and an unknown one). Since E9.5 S1 the gates are order-independent — read both flags, never
|
||||
* account and an unknown one). The gates are order-independent — read both flags, never
|
||||
* infer one from the other. */
|
||||
export type LoginBlocked = {
|
||||
reason: "email_unverified" | "pending_approval" | "suspended" | "invalid" | "rate_limited";
|
||||
@@ -101,8 +101,8 @@ export const authApi = {
|
||||
// Confirm an email-verification token the SPA read from the URL fragment (SB3).
|
||||
// `password_reset_required` = SEC-10 dropped the password this account registered with, so signing
|
||||
// in would fail with the uniform credentials error. The card sends the user to the reset flow instead
|
||||
// of leaving them to guess. Rare since E9.5 S1: only accounts stripped before that change, and the
|
||||
// Google-adoption path, still reach it.
|
||||
// of leaving them to guess. Rare now: only accounts stripped before the gates became
|
||||
// order-independent, and the Google-adoption path, still reach it.
|
||||
// `email` is the address that was just confirmed — echoed back so the banner can name it, which
|
||||
// matters when the browser holds a session for a DIFFERENT account.
|
||||
verifyEmail: (
|
||||
|
||||
@@ -97,7 +97,7 @@ describe("uploadReq", () => {
|
||||
// Both abort producers are asserted through `isAborted`, not through `{status: 0}`: what the
|
||||
// upload dialog reads is the SENTINEL, and a cancelled upload that stops matching it becomes a
|
||||
// red "the upload failed" toast. Changing either `reject` to a friendlier message left every
|
||||
// test green, because the only consumer test hand-builds the error it expects (round 6).
|
||||
// test green, because the only consumer test hand-builds the error it expects.
|
||||
it("rejects immediately if the signal is already aborted (never sends)", async () => {
|
||||
const ac = new AbortController();
|
||||
ac.abort();
|
||||
@@ -403,7 +403,7 @@ describe("req — what it has already put in front of the user", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// E16: while a debug session records, every request path names it — the server tags ITS lines of
|
||||
// While a debug session records, every request path names it — the server tags ITS lines of
|
||||
// that request with it. A path that forgot the header would leave holes in the evidence exactly
|
||||
// where the upload (XHR) or the unload beacon happened.
|
||||
describe("the debug session a tab records into", () => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { currentDebugSession, newRequestId } from "../log";
|
||||
/** The active UI language, as a bare code the server understands ("en"/"hu" — never "en-GB").
|
||||
* Lives here so the request builders can stamp it themselves: a signal that has to be passed in by
|
||||
* every caller is one every caller can forget, which is how the registration language shipped as a
|
||||
* field nobody ever filled (E9.5 S6 → S7). */
|
||||
* field nobody ever filled. */
|
||||
export const uiLang = (): string => i18n.language.split("-")[0] || "en";
|
||||
|
||||
// Errors `req` has already put in front of the user for good — the blocking error dialog, or the
|
||||
@@ -24,7 +24,7 @@ export const uiLang = (): string => i18n.language.split("-")[0] || "en";
|
||||
// NOT the connection notice. It says the server is unreachable, not that this action failed, and it
|
||||
// removes itself on the next answer — which every caller's own resync refetch fetches at once. Marked
|
||||
// as reported, a delete that hit a keepalive reset (a 502) flashed the notice for a moment and was
|
||||
// then reported nowhere at all, the list still there (deep-review round 4).
|
||||
// then reported nowhere at all, the list still there.
|
||||
//
|
||||
// A caller that reports a failure itself marks it too (`notifyActionError` does), which is what lets
|
||||
// the ONE app-wide mutation reporter (lib/queryClient) stay quiet for a mutation whose own `onError`
|
||||
@@ -96,7 +96,7 @@ function localizeDetail(d: unknown): string {
|
||||
return t(`downloads.errors.edit.${o.reason}`, t("downloads.errors.edit.generic"));
|
||||
if (o?.code === "cookies")
|
||||
return t(`downloads.errors.cookies.${o.reason}`, t("downloads.errors.cookies.generic"));
|
||||
// Password sign-in refusals (E9.5 S4). The card renders the account-shaped ones as a full panel
|
||||
// Password sign-in refusals. The card renders the account-shaped ones as a full panel
|
||||
// off `detailData`; this is the one-line form every other consumer (and the card's own fallback)
|
||||
// gets. A reason this build doesn't know falls through to the generic message rather than showing
|
||||
// a raw key, which is what an i18n miss would otherwise put on screen.
|
||||
@@ -109,7 +109,7 @@ function localizeDetail(d: unknown): string {
|
||||
return o.reason === "weak_password"
|
||||
? t("welcome.auth.weakPassword", { min: o.limit ?? o.min })
|
||||
: t("welcome.auth.deadLink.short");
|
||||
// Local-upload ingest (E7 S5): the per-file cap carries the limit in bytes; empty is a bare code.
|
||||
// Local-upload ingest: the per-file cap carries the limit in bytes; empty is a bare code.
|
||||
if (o?.code === "upload_too_large")
|
||||
return t("downloads.errors.upload.tooLarge", { size: formatBytes(o.limit, i18n.language) });
|
||||
if (o?.code === "upload_empty") return t("downloads.errors.upload.empty");
|
||||
@@ -136,7 +136,7 @@ function localizeDetail(d: unknown): string {
|
||||
// from that address" — so an unknown reason falls back to the generic refusal rather than a key.
|
||||
if (o?.code === "url_blocked")
|
||||
return t(`downloads.errors.urlBlocked.${o.reason}`, t("downloads.errors.urlBlocked.generic"));
|
||||
// E10 S2: the one-step "move to another list". Both reasons mean the same thing to a reader —
|
||||
// The one-step "move to another list". Both reasons mean the same thing to a reader —
|
||||
// the list moved under you — but they are different enough to be worth naming separately, and an
|
||||
// unknown reason falls back to the plain failure rather than printing a key.
|
||||
if (o?.code === "playlist_move")
|
||||
@@ -267,7 +267,7 @@ export const setupHeaders = (token: string) => ({
|
||||
// sessionStorage (per-tab, survives F5, not shared across tabs). null = use the session default.
|
||||
// The storage key + reader live in storage.ts (so its per-account helpers can use them too).
|
||||
const ACTIVE_ACCOUNT_HEADER = "X-Siftlode-Account";
|
||||
// The debug session this tab is recording (E16): the server tags its own lines of the request with
|
||||
// The debug session this tab is recording: the server tags its own lines of the request with
|
||||
// it, and logs them at the session's level. Ignored by the server unless it is the caller's own.
|
||||
const DEBUG_SESSION_HEADER = "X-Debug-Session";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The user's diagnostics level (E22 S5) and the browser log it lets out. Only touches core's req and
|
||||
// The user's diagnostics level and the browser log it lets out. Only touches core's req and
|
||||
// beacon. The streaming itself — what is sent, when, and when it stops — is `lib/diagnostics.ts`.
|
||||
import type { LogEvent } from "../log";
|
||||
import type { ServerLogRow } from "./admin";
|
||||
@@ -37,7 +37,7 @@ export const diagnosticsApi = {
|
||||
sendLogEventsOnUnload: (events: LogEvent[]): void => beacon(DIAGNOSTICS_EVENTS_PATH, { events }),
|
||||
};
|
||||
|
||||
// --- debug sessions (E16) ----------------------------------------------------------------------
|
||||
// --- debug sessions ----------------------------------------------------------------------
|
||||
|
||||
/** The two levels a recording can run at — it always records more than the base. */
|
||||
export type DebugSessionLevel = "DEBUG" | "TRACE";
|
||||
|
||||
@@ -51,7 +51,7 @@ describe("discoverApi query strings", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("the reader's language (E30)", () => {
|
||||
describe("the reader's language", () => {
|
||||
it("asks for Hungarian Plex cards on every Home read, and leaves English implied", async () => {
|
||||
await discoverApi.discover("any", 0, "hu");
|
||||
expect(lastUrl()).toContain("lang=hu");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The Home surface ("Mit nézzek most?", E24): one hero suggestion, a cross-source resume rail, and
|
||||
// The Home surface ("Mit nézzek most?"): one hero suggestion, a cross-source resume rail, and
|
||||
// the comfort shelf. Types mirror `backend/app/discover/` — the card carries the NATIVE feed / Plex
|
||||
// payload under `video` / `plex`, so the page renders through the existing VideoCard /
|
||||
// PlexPosterCard instead of a third card component.
|
||||
@@ -71,7 +71,7 @@ export interface DiscoverPayload {
|
||||
}
|
||||
|
||||
export const discoverApi = {
|
||||
// `lang` "hu" serves a Plex card's Hungarian title (E30); en is the server default.
|
||||
// `lang` "hu" serves a Plex card's Hungarian title; en is the server default.
|
||||
discover: (budget: DiscoverBudget, seed: number, lang?: string): Promise<DiscoverPayload> =>
|
||||
req(`/api/discover?budget=${budget}&seed=${seed}${lang === "hu" ? "&lang=hu" : ""}`),
|
||||
// The "Mást kérek" reroll: a new hero alone, so pressing it does not re-fetch the rails under the
|
||||
|
||||
@@ -194,7 +194,7 @@ export const downloadsApi = {
|
||||
spec?: DownloadSpec;
|
||||
display_name?: string;
|
||||
}): Promise<DownloadJob> => req("/api/downloads", { method: "POST", body: JSON.stringify(body) }),
|
||||
// Ingest a local file into the library (E7 S5). Sent via XHR rather than `req` so the dialog can
|
||||
// Ingest a local file into the library. Sent via XHR rather than `req` so the dialog can
|
||||
// show upload progress; any file type is accepted (media gets the full function set, a generic
|
||||
// file is download-only). Returns the already-`done` job the ingest creates.
|
||||
// The body IS the file, and the two names ride as headers. Not a style choice: a multipart part is
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
|
||||
// The R7.5 guard: lib/api.ts is a spread-only façade over per-domain slice modules. These tests
|
||||
// The façade guard: lib/api.ts is a spread-only façade over per-domain slice modules. These tests
|
||||
// mechanise the two invariants that make that structure safe — so a future edit that reintroduces
|
||||
// the interleave (a method dropped into two slices → a silent last-wins spread override) or bloats
|
||||
// the barrel (a method defined inline instead of in a domain module) fails CI instead of shipping.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// E32 S2: lib/api/feed was barely covered, and it is where a filter turns into a query string.
|
||||
// Lib/api/feed was barely covered, and it is where a filter turns into a query string.
|
||||
// FastAPI IGNORES a query parameter it does not know, so a misspelt or renamed key does not fail —
|
||||
// the filter simply stops applying while its chip still shows. Locked two ways: what each filter
|
||||
// emits, and that every key emitted is one the backend's feed endpoints actually read.
|
||||
|
||||
@@ -99,7 +99,7 @@ export interface FeedFilters {
|
||||
year?: number;
|
||||
}
|
||||
|
||||
/** One year's video count for the year control's chips + "more…" dialog (item 8 / E8·S2). */
|
||||
/** One year's video count for the year control's chips + "more…" dialog. */
|
||||
export interface YearCount {
|
||||
year: number;
|
||||
count: number;
|
||||
@@ -169,7 +169,7 @@ export const feedApi = {
|
||||
// the separate feedCount + facets the feed and sidebar used to fire on every filter change (PERF-13).
|
||||
feedSummary: (f: FeedFilters): Promise<{ count: number; facets: Record<string, number> }> =>
|
||||
req(`/api/feed/summary?${filterParams(f).toString()}`),
|
||||
// Per-year video counts for the year control (item 8 / E8·S2). The backend drops the publish-date
|
||||
// Per-year video counts for the year control. The backend drops the publish-date
|
||||
// narrowing, so callers key this on the date-stripped filters (picking a year mustn't refetch it).
|
||||
feedYears: (f: FeedFilters): Promise<{ years: YearCount[] }> =>
|
||||
req(`/api/feed/years?${filterParams(f).toString()}`),
|
||||
|
||||
@@ -55,8 +55,8 @@ export interface PlexCard {
|
||||
show_id?: string | null; // episode: the show's rating_key (for grouping a playlist by show/season)
|
||||
summary?: string | null; // movie/show hover-overview synopsis (trimmed) + episode summary
|
||||
}
|
||||
// TMDB enrichment block (E5 S2), served from OUR DB (never a live TMDB call) on a movie/show detail.
|
||||
// null until the row has been enriched. Powers the S5 enriched detail page: multi-source ratings row,
|
||||
// TMDB enrichment block, served from OUR DB (never a live TMDB call) on a movie/show detail.
|
||||
// null until the row has been enriched. Powers the enriched detail page: multi-source ratings row,
|
||||
// facts sidebar, keyword chips (→ filter), and the full crew.
|
||||
interface PlexTmdbCrew {
|
||||
name: string;
|
||||
@@ -73,7 +73,7 @@ export interface PlexTmdb {
|
||||
keywords: string[];
|
||||
crew: PlexTmdbCrew[];
|
||||
studios: string[];
|
||||
// ISO code (null on a row enriched before E30 whose name TMDB's list lacked) + TMDB's English name.
|
||||
// ISO code (null on an older enriched row whose name TMDB's list lacked) + TMDB's English name.
|
||||
production_countries: { code: string | null; name: string }[];
|
||||
recommendations: { tmdb_id?: number | null; title?: string | null; in_library: boolean }[];
|
||||
backdrops: string[];
|
||||
@@ -133,7 +133,7 @@ export interface PlexMarker {
|
||||
start_s: number;
|
||||
end_s: number;
|
||||
}
|
||||
// What the player names an audio/subtitle track from (E30, `lib/vocab` trackLabel). Plex's own title
|
||||
// What the player names an audio/subtitle track from (`lib/vocab` trackLabel). Plex's own title
|
||||
// (`label`) is in the Plex SERVER's language, so it is only the fallback for a track with no language tag.
|
||||
export interface PlexTrackNaming {
|
||||
ord: number;
|
||||
@@ -239,7 +239,7 @@ export interface PlexPersonCard {
|
||||
export interface PlexPersonDetail {
|
||||
name: string;
|
||||
photo?: string | null; // cached headshot, or null (placeholder)
|
||||
// TMDB bio (E5 S6), fetched ahead of time by the people_enrich job (E30 S6) and served from the row.
|
||||
// TMDB bio, fetched ahead of time by the people_enrich job and served from the row.
|
||||
// Null when the person has no linked tmdb_person_id (never appeared in a TMDB-enriched title), TMDB
|
||||
// has none, or it is still on its way (`enriching`).
|
||||
biography?: string | null;
|
||||
@@ -270,7 +270,7 @@ export interface PlexFilters {
|
||||
actorMode?: "any" | "all"; // how multiple actors combine (default any = OR)
|
||||
studios: string[]; // OR — from any selected studio
|
||||
playable: string[]; // OR — playback class (direct | remux | transcode_hw | transcode_sw); movie-only
|
||||
keywords: string[]; // AND — TMDB keyword chips from the detail page (E5 S5)
|
||||
keywords: string[]; // AND — TMDB keyword chips from the detail page
|
||||
collections: string[]; // Plex collection rating_keys; a title can be in several
|
||||
collectionMode?: "any" | "all"; // how multiple collections combine (default any = OR)
|
||||
collectionTitles?: Record<string, string>; // rating_key → title, for the active-filter chips
|
||||
@@ -400,7 +400,7 @@ export function parsePlexFilters(p: URLSearchParams): PlexFilters {
|
||||
export interface PlexFacets {
|
||||
genres: { value: string; count: number }[];
|
||||
content_ratings: { value: string; count: number }[];
|
||||
studios: { value: string; count: number }[]; // studio vocabulary for the sidebar picker (E5 S7)
|
||||
studios: { value: string; count: number }[]; // studio vocabulary for the sidebar picker
|
||||
// Playback-class facet (movie-only): value ∈ direct|remux|transcode_hw|transcode_sw. Empty on shows.
|
||||
playable: { value: string; count: number }[];
|
||||
year_min: number | null;
|
||||
@@ -418,7 +418,7 @@ export interface PlexPlaySession {
|
||||
tonemap?: boolean; // hls HW transcode only: 10-bit/HDR source tone-mapped HDR→SDR on the GPU (OpenCL)
|
||||
}
|
||||
|
||||
// Discover surface (E5 S4): the consolidated rail payload. Every rail is a plain PlexCard list the
|
||||
// Discover surface: the consolidated rail payload. Every rail is a plain PlexCard list the
|
||||
// dedicated Discover landing renders as a horizontal Carousel; `because_you_watched` is null when the
|
||||
// user has no TMDB-enriched watch history (rail hidden). Served from DB — no live Plex/TMDB call.
|
||||
export interface PlexDiscover {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { plexFiltersToParams, type PlexFilters } from "./plex";
|
||||
|
||||
// FastAPI ignores a query parameter it does not know, so a Plex filter key the frontend misspells or
|
||||
// the backend renames does not fail anywhere — the filter silently stops narrowing while its chip
|
||||
// still shows (E32 S2, the sibling of lib/api/feed.test.ts). Every key the filter set can put on the
|
||||
// still shows (the sibling of lib/api/feed.test.ts). Every key the filter set can put on the
|
||||
// wire must be a field of the backend's LibraryFilters, which /library and /facets both inject.
|
||||
// Required: a new filter field fails tsc here until it has a value, so the checks below see its key.
|
||||
const EVERYTHING: Required<PlexFilters> = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// E30: every Plex read that returns cards carries the reader's language, so the server can serve the
|
||||
// Every Plex read that returns cards carries the reader's language, so the server can serve the
|
||||
// Hungarian titles. The grid and the detail pages always did; these reads did not.
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { plexApi } from "./plex";
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface Tag {
|
||||
channel_count: number;
|
||||
}
|
||||
|
||||
// One suggested user-tag for a channel, learned from the user's own manual-tag pattern (E8·S4).
|
||||
// One suggested user-tag for a channel, learned from the user's own manual-tag pattern.
|
||||
interface TagSuggestion {
|
||||
tag_id: number;
|
||||
confidence: number;
|
||||
|
||||
Reference in New Issue
Block a user