- lib/vocab + the `vocab` locale area: TMDB crew jobs and departments, auto-tags (D1 A: the data
stays English, the catalog is named at its one source, useTags), built-in download profiles,
YouTube topics; held key-for-key to the backend's own lists by backendIds.contract.
- Countries by ISO code: TMDB parse stores {code, name}; rows stored before are coded from TMDB's
own country list in one request (run_enrich); Intl names them, historic codes (YU, SU, XI…)
from the catalog.
- Player tracks named from the language tag, forced/SDH flags and codec tail, not Plex's
server-language displayTitle; no invented "Audio N" on the server.
- Download lifecycle notices (shared / expiring / expired / evicted) named from data; the share
notice carries the title.
- Scheduler: the job result rides along as data and is named per key (dashboard + run notice).
- Invite status named; a render crash's notice no longer shows the engine's English message.
- Fix: YouTube links …/wiki/Humour, the tagger looked for "Humor" — Comedy never came from a topic.
62 lines
2.2 KiB
TypeScript
62 lines
2.2 KiB
TypeScript
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
import i18n from "../i18n";
|
|
import { notify } from "../lib/notifications";
|
|
import { errorAttrs, log } from "../lib/log";
|
|
import { reloadingForNewBuild } from "../lib/staleBuild";
|
|
import { Button } from "./ui/Button";
|
|
|
|
interface Props {
|
|
children: ReactNode;
|
|
}
|
|
interface State {
|
|
crashed: boolean;
|
|
}
|
|
|
|
// Catches render-time crashes anywhere in the tree, logs a fatal notification (kept
|
|
// in the center across the reload) and offers a recovery action.
|
|
export default class ErrorBoundary extends Component<Props, State> {
|
|
state: State = { crashed: false };
|
|
|
|
static getDerivedStateFromError(): State {
|
|
return { crashed: true };
|
|
}
|
|
|
|
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
// The tab is already reloading onto a newer build (lib/staleBuild): this is the chunk it could not
|
|
// load, not a crash — no fatal notice, which would outlive the reload.
|
|
if (reloadingForNewBuild()) return;
|
|
log.fatal("ui.crash", "the app crashed while rendering", {
|
|
...errorAttrs(error),
|
|
...(info.componentStack ? { component_stack: info.componentStack } : {}),
|
|
});
|
|
notify({
|
|
level: "fatal",
|
|
title: i18n.t("errors.boundary.notifTitle"),
|
|
// Never the raw `error.message`: a JS engine's English ("Cannot read properties of undefined…")
|
|
// is no message for the reader (E30) — the `ui.crash` log above carries it for diagnosis.
|
|
message: i18n.t("errors.boundary.notifMessage"),
|
|
requiresInteraction: true,
|
|
});
|
|
}
|
|
|
|
render() {
|
|
if (this.state.crashed) {
|
|
if (reloadingForNewBuild()) return null;
|
|
return (
|
|
<div className="min-h-dvh grid place-items-center text-muted p-6 text-center">
|
|
<div>
|
|
<div className="text-lg font-semibold text-fg mb-1">
|
|
{i18n.t("errors.boundary.title")}
|
|
</div>
|
|
<div className="text-sm mb-3">{i18n.t("errors.boundary.subtitle")}</div>
|
|
<Button variant="link" onClick={() => location.reload()} className="text-sm">
|
|
{i18n.t("errors.boundary.reload")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
return this.props.children;
|
|
}
|
|
}
|