A user can raise their diagnostics level to Részletes (DEBUG) or Teljes (TRACE) for 24 hours from a new Settings tab, after a consent box that says what is recorded, that the server keeps it and for how long (the instance's own retention), and that it lapses. Alap is the base and needs no consent. - users.diagnostics_level/_until (migration 0087), written only by PUT /api/me/diagnostics, which computes the expiry itself; the demo account cannot raise it. /api/me reports the level in force. - The server logs an elevated user's requests at that level and stores every line of them (logs.LevelGate on the stdout handler and the sink; the root is lowered only while an elevation is active). Teljes adds the redacted query string to the request line. - POST /api/diagnostics/events stores the browser's log as source=web rows: account from the session, body capped while streaming, events/attrs capped, re-redacted, events under the consented level dropped, timestamps clamped, 30 batches a minute, 409 once the level lapsed. - lib/diagnostics.ts applies the level to the tab and streams only what happens after consent, in batches, never its own request lines, with a keepalive flush on pagehide; stops on lapse (its own timer), on withdrawal and on 409. - Detail hooks at DEBUG: which control was clicked (test id, icon, app copy in English — never user text, input values or a link's query), visibility/bfcache/resize, filter changes (lib/filterLog.ts; typed search only at TRACE), player events (lib/mediaLog.ts; media errors are warnings at every level). Offline/online at the base level. - "Saját napló" viewer: live, newest first, level groups with counts, search, row details, copy the filtered events as JSON lines, clear. - Privacy policy and Terms exist in Hungarian and English (a contract test holds both languages to the same sections); the policy now describes the server logs and diagnostic logging, self-service deletion and password sign-in. Fixed on the way: - A sync auth dependency ran logs.set_user on a copied context, so the account never reached a handler's own log lines (measured); a per-request mutable scope carries it now. - The Settings tab rail left a phone no room (clipped labels, buttons over text): below `sm` it is a scrolling row above the content. - PUT /api/me/preferences took a bare dict body; it is a Pydantic model now.
44 lines
2.1 KiB
TypeScript
44 lines
2.1 KiB
TypeScript
import i18n from "./index";
|
|
|
|
// The translation KEY a piece of on-screen text is, or null (E22 S5 click capture).
|
|
//
|
|
// A click is logged by what the control IS, and its visible label is the most telling part — but a
|
|
// label can be the user's own content (a video title, a tag, a channel name), which the log must not
|
|
// carry. Text that is exactly one of the app's own translated strings is app copy, not user content,
|
|
// so it can be logged — as its ENGLISH wording, whatever the UI language: the same click reads the
|
|
// same in every log, and nothing a user typed comes out of it (a user's tag that happens to equal a
|
|
// string of ours is logged as our string, which says nothing about the user). Not as a key: one
|
|
// wording is often several keys ("Diagnostics" is a settings tab AND a config group), so a key would
|
|
// name a control the user may never have touched. Interpolated strings (`{{n}} items`) never match.
|
|
let index: Map<string, string> | null = null;
|
|
|
|
const normalize = (text: string) => text.replace(/\s+/g, " ").trim();
|
|
|
|
/** Every app string (any language) → its English wording. */
|
|
function build(): Map<string, string> {
|
|
const map = new Map<string, string>();
|
|
const resources = i18n.options.resources ?? {};
|
|
const english = (path: string[]): string | null => {
|
|
let node: unknown = resources.en?.translation;
|
|
for (const key of path) node = (node as Record<string, unknown> | undefined)?.[key];
|
|
return typeof node === "string" ? normalize(node) : null;
|
|
};
|
|
const walk = (node: unknown, path: string[]) => {
|
|
if (typeof node === "string") {
|
|
const text = normalize(node);
|
|
const en = english(path);
|
|
if (en && text.length >= 2 && !text.includes("{{") && !map.has(text)) map.set(text, en);
|
|
} else if (node && typeof node === "object") {
|
|
for (const [k, v] of Object.entries(node)) walk(v, [...path, k]);
|
|
}
|
|
};
|
|
for (const lang of Object.keys(resources)) walk(resources[lang]?.translation, []);
|
|
return map;
|
|
}
|
|
|
|
export function appCopyOf(text: string | null | undefined): string | null {
|
|
if (!text) return null;
|
|
index ??= build();
|
|
return index.get(normalize(text)) ?? null;
|
|
}
|