docs(34): add code review report
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
---
|
||||
phase: 34-i18n-foundation
|
||||
reviewed: 2026-04-17T00:00:00Z
|
||||
reviewed: 2026-04-18T14:30:00Z
|
||||
depth: standard
|
||||
files_reviewed: 23
|
||||
files_reviewed: 40
|
||||
files_reviewed_list:
|
||||
- package.json
|
||||
- src/client/components/AddToCollectionModal.tsx
|
||||
- src/client/components/ClassificationBadge.tsx
|
||||
- src/client/components/ImageUpload.tsx
|
||||
- src/client/components/ImpactDeltaBadge.tsx
|
||||
@@ -13,282 +15,169 @@ files_reviewed_list:
|
||||
- src/client/components/ThreadCard.tsx
|
||||
- src/client/components/ThreadTabs.tsx
|
||||
- src/client/components/TotalsBar.tsx
|
||||
- src/client/hooks/useFormatters.ts
|
||||
- src/client/hooks/useLanguage.ts
|
||||
- src/client/lib/formatters.ts
|
||||
- src/client/lib/i18n.ts
|
||||
- src/client/locales/de/catalog.json
|
||||
- src/client/locales/de/collection.json
|
||||
- src/client/locales/de/common.json
|
||||
- src/client/locales/de/onboarding.json
|
||||
- src/client/locales/de/settings.json
|
||||
- src/client/locales/de/setups.json
|
||||
- src/client/locales/de/threads.json
|
||||
- src/client/locales/en/catalog.json
|
||||
- src/client/locales/en/collection.json
|
||||
- src/client/locales/en/common.json
|
||||
- src/client/locales/en/onboarding.json
|
||||
- src/client/locales/en/settings.json
|
||||
- src/client/locales/en/setups.json
|
||||
- src/client/locales/en/threads.json
|
||||
- src/client/main.tsx
|
||||
- src/client/routes/__root.tsx
|
||||
- src/client/routes/collection/index.tsx
|
||||
- src/client/routes/global-items/index.tsx
|
||||
- src/client/routes/index.tsx
|
||||
- src/client/routes/items/$itemId.tsx
|
||||
- src/client/routes/profile.tsx
|
||||
- src/client/routes/settings.tsx
|
||||
- src/client/routes/setups/$setupId.tsx
|
||||
- src/client/routes/threads/$threadId/index.tsx
|
||||
- src/client/routes/users/$userId.tsx
|
||||
- tests/formatters.test.ts
|
||||
findings:
|
||||
critical: 0
|
||||
warning: 7
|
||||
info: 4
|
||||
total: 11
|
||||
critical: 1
|
||||
warning: 3
|
||||
info: 3
|
||||
total: 7
|
||||
status: issues_found
|
||||
---
|
||||
|
||||
# Phase 34: Code Review Report
|
||||
|
||||
**Reviewed:** 2026-04-17T00:00:00Z
|
||||
**Reviewed:** 2026-04-18T14:30:00Z
|
||||
**Depth:** standard
|
||||
**Files Reviewed:** 23
|
||||
**Files Reviewed:** 40
|
||||
**Status:** issues_found
|
||||
|
||||
## Summary
|
||||
|
||||
This review covers the i18n foundation work: wiring components to `react-i18next`, locale JSON files for English and German, and three routes (`index`, `profile`, `settings`). The EN locale is complete and internally consistent. The DE locale has significant gaps — several keys used by components exist only in EN, meaning German users will see raw key strings or English fallback text in multiple places. Additionally, one component hardcodes a locale in a `toLocaleDateString` call (bypassing i18n entirely), one component shadows the `t` translation function with a filter callback variable, and two hardcoded English strings in `ImageUpload` were not extracted to the locale.
|
||||
This review covers the i18n foundation implementation across 40 files: the i18n library setup, locale JSON files (English and German), React components and routes using `useTranslation`, formatting utilities, and associated tests. The i18n architecture is well-structured with namespace separation, proper fallback language configuration, and locale-aware formatting.
|
||||
|
||||
---
|
||||
One critical bug was found where the account deletion confirmation check is hardcoded to English ("DELETE") but the German locale instructs users to type "LOSCHEN", making account deletion impossible for German-language users. Several warnings address incomplete i18n adoption (hardcoded locale in date formatting, hardcoded English placeholder strings) and a subtle falsy-value bug. Info items cover variable shadowing and a debug `console.error` statement.
|
||||
|
||||
## Critical Issues
|
||||
|
||||
### CR-01: Account Deletion Confirmation Hardcoded to English
|
||||
|
||||
**File:** `src/client/routes/profile.tsx:380`
|
||||
**Issue:** The delete account confirmation checks `confirmation !== "DELETE"` but the German locale (`de/common.json:118-119`) instructs users to type "LOSCHEN". German-language users cannot delete their accounts because the hardcoded string comparison will never match their input.
|
||||
**Fix:** Use a locale-aware confirmation word, or always use "DELETE" in both locale files:
|
||||
|
||||
Option A -- Use a translation key for the confirmation word:
|
||||
```tsx
|
||||
// Add to common.json: "deleteConfirmWord": "DELETE" (en), "deleteConfirmWord": "LOSCHEN" (de)
|
||||
const confirmWord = t("profile.deleteConfirmWord");
|
||||
// ...
|
||||
disabled={confirmation !== confirmWord || deleteAccount.isPending}
|
||||
```
|
||||
|
||||
Option B -- Standardize on "DELETE" in both locales:
|
||||
```json
|
||||
// de/common.json
|
||||
"deleteConfirmMessage": "Diese Aktion ist dauerhaft. Geben Sie DELETE ein, um zu bestätigen.",
|
||||
"deleteConfirmPlaceholder": "Geben Sie DELETE ein, um zu bestätigen"
|
||||
```
|
||||
|
||||
## Warnings
|
||||
|
||||
### WR-01: DE locale missing `tabs.setups`, `totals`, and `classificationBadge` keys
|
||||
### WR-01: Hardcoded Locale in ThreadCard Date Formatting
|
||||
|
||||
**File:** `src/client/locales/de/collection.json`
|
||||
**Issue:** The EN `collection.json` contains three top-level sections absent from the DE file: `tabs` (`tabs.setups`), `totals` (`totals.totalWeight`, `totals.totalCost`), and `classificationBadge` (`classificationBadge.base`, `classificationBadge.worn`, `classificationBadge.consumable`). `CollectionTabs` calls `t("tabs.setups")` and `ClassificationBadge` calls `t("classificationBadge.base")` as a `defaultValue`. When the language is German, these keys resolve to the raw key string unless a fallback language is configured in i18next.
|
||||
**Fix:** Add the missing keys to `de/collection.json`:
|
||||
```json
|
||||
"tabs": {
|
||||
"setups": "Setups"
|
||||
},
|
||||
"totals": {
|
||||
"totalWeight": "Gesamtgewicht",
|
||||
"totalCost": "Gesamtkosten"
|
||||
},
|
||||
"classificationBadge": {
|
||||
"base": "Basisgewicht",
|
||||
"worn": "Getragen",
|
||||
"consumable": "Verbrauchsmaterial"
|
||||
}
|
||||
```
|
||||
|
||||
### WR-02: DE locale missing `card.by`, `card.anonymous`, `impact.compareWith` in setups
|
||||
|
||||
**File:** `src/client/locales/de/setups.json`
|
||||
**Issue:** The EN `setups.json` contains `card.by`, `card.anonymous`, and `impact.compareWith`. The DE file omits all three. `PublicSetupCard` calls `t("card.by", { name: ... })` and `t("card.anonymous")` unconditionally; `SetupImpactSelector` calls `t("impact.compareWith")` as the blank option label. German users will see raw key strings in these locations.
|
||||
**Fix:** Add to `de/setups.json`:
|
||||
```json
|
||||
"card": {
|
||||
"items": "{{count}} Gegenstände",
|
||||
"items_one": "{{count}} Gegenstand",
|
||||
"weight": "Gewicht",
|
||||
"price": "Preis",
|
||||
"by": "von {{name}}",
|
||||
"anonymous": "Anonym"
|
||||
},
|
||||
"impact": {
|
||||
"title": "Auswirkungsvorschau",
|
||||
"adding": "Hinzufügen",
|
||||
"removing": "Entfernen",
|
||||
"compareWith": "Mit Setup vergleichen..."
|
||||
}
|
||||
```
|
||||
|
||||
### WR-03: DE locale missing `card.candidates` and `planning.*` keys in threads
|
||||
|
||||
**File:** `src/client/locales/de/threads.json`
|
||||
**Issue:** The EN `threads.json` contains `card.candidates` (with pluralisation) and the entire `planning.*` subtree (8 keys). `ThreadCard` calls `t("card.candidates", { count: candidateCount })` and `PlanningView` calls `t("threads:planning.title")`, `t("threads:planning.emptyTitle")`, `t("threads:planning.createFirst")`, and three sets of step titles/descriptions. All of these fall back to raw key strings in German.
|
||||
**Fix:** Add to `de/threads.json`:
|
||||
```json
|
||||
"card": {
|
||||
"candidates": "{{count}} Kandidaten",
|
||||
"candidates_one": "{{count}} Kandidat"
|
||||
},
|
||||
"planning": {
|
||||
"title": "Planungs-Threads",
|
||||
"emptyTitle": "Planen Sie Ihren nächsten Kauf",
|
||||
"createFirst": "Ersten Thread erstellen",
|
||||
"step1Title": "Thread erstellen",
|
||||
"step1Description": "Starten Sie einen Recherche-Thread für Ausrüstung, die Sie in Betracht ziehen",
|
||||
"step2Title": "Kandidaten hinzufügen",
|
||||
"step2Description": "Fügen Sie Produkte hinzu, die Sie mit Preisen und Gewichten vergleichen",
|
||||
"step3Title": "Gewinner wählen",
|
||||
"step3Description": "Schließen Sie den Thread ab und der Gewinner wird Ihrer Sammlung hinzugefügt"
|
||||
}
|
||||
```
|
||||
|
||||
### WR-04: DE locale missing `currency.suggestion`, `currency.switch`, and `showConversions` keys in settings
|
||||
|
||||
**File:** `src/client/locales/de/settings.json`
|
||||
**Issue:** The EN `settings.json` contains `currency.suggestion`, `currency.switch`, and the entire `showConversions` block. `SettingsPage` renders a currency suggestion banner using `t("currency.suggestion", { symbol, code })` and `t("currency.switch")`, and the "Show Converted Prices" toggle uses `t("showConversions.title")` and `t("showConversions.description")`. German users see raw key strings for all four of these.
|
||||
**Fix:** Add to `de/settings.json`:
|
||||
```json
|
||||
"currency": {
|
||||
"title": "Währung",
|
||||
"description": "Ändert das angezeigte Währungssymbol. Werte werden nicht umgerechnet.",
|
||||
"suggestion": "Basierend auf Ihrer Region empfehlen wir {{symbol}} ({{code}})",
|
||||
"switch": "Wechseln"
|
||||
},
|
||||
"showConversions": {
|
||||
"title": "Umgerechnete Preise anzeigen",
|
||||
"description": "Ungefähre Umrechnungen anzeigen, wenn kein lokaler Preis verfügbar ist"
|
||||
}
|
||||
```
|
||||
|
||||
### WR-05: DE locale missing `home.*`, `imageUpload.*`, and `profile.*` keys in common
|
||||
|
||||
**File:** `src/client/locales/de/common.json`
|
||||
**Issue:** The EN `common.json` contains three top-level sections absent from DE: `home` (3 keys used by `index.tsx`), `imageUpload` (4 keys used by `ImageUpload.tsx`), and `profile` (22 keys used by `profile.tsx`). These are the most user-facing gaps — the entire profile page renders raw key strings in German.
|
||||
**Fix:** Add the missing sections to `de/common.json`. The `profile` section is large; key translations include:
|
||||
```json
|
||||
"home": {
|
||||
"popularSetups": "Beliebte Setups",
|
||||
"recentlyAdded": "Kürzlich hinzugefügt",
|
||||
"trendingCategories": "Beliebte Kategorien"
|
||||
},
|
||||
"imageUpload": {
|
||||
"clickToAdd": "Klicken zum Hinzufügen eines Fotos",
|
||||
"invalidType": "Bitte wählen Sie ein JPG-, PNG- oder WebP-Bild.",
|
||||
"tooLarge": "Das Bild muss kleiner als 5 MB sein.",
|
||||
"uploadFailed": "Upload fehlgeschlagen. Bitte versuchen Sie es erneut."
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profil",
|
||||
"account": "Konto",
|
||||
"accountInfo": "Ihre Kontoinformationen",
|
||||
"email": "E-Mail",
|
||||
"noEmail": "Keine E-Mail-Adresse hinterlegt",
|
||||
"change": "Ändern",
|
||||
"newEmailPlaceholder": "Neue E-Mail-Adresse",
|
||||
"updating": "Wird aktualisiert...",
|
||||
"updateEmail": "E-Mail aktualisieren",
|
||||
"emailUpdated": "E-Mail aktualisiert",
|
||||
"memberSince": "Mitglied seit",
|
||||
"security": "Sicherheit",
|
||||
"managePassword": "Ihr Passwort verwalten",
|
||||
"currentPassword": "Aktuelles Passwort",
|
||||
"newPassword": "Neues Passwort",
|
||||
"password": "Passwort",
|
||||
"confirmPassword": "Passwort bestätigen",
|
||||
"passwordRequirements": "Das Passwort muss mindestens 8 Zeichen mit Groß-, Kleinbuchstaben und einer Zahl enthalten.",
|
||||
"passwordUpdated": "Passwort aktualisiert",
|
||||
"changingPassword": "Wird geändert...",
|
||||
"changePassword": "Passwort ändern",
|
||||
"setPassword": "Passwort festlegen",
|
||||
"dangerZone": "Gefahrenzone",
|
||||
"dangerZoneDescription": "Löschen Sie Ihr Konto und alle persönlichen Daten. Öffentliche Setups werden \"Gelöschter Benutzer\" zugeordnet.",
|
||||
"deleteAccount": "Konto löschen",
|
||||
"deleteConfirmMessage": "Diese Aktion ist dauerhaft. Geben Sie LÖSCHEN ein, um zu bestätigen.",
|
||||
"deleteConfirmPlaceholder": "LÖSCHEN eingeben, um zu bestätigen"
|
||||
}
|
||||
```
|
||||
|
||||
### WR-06: `ThreadCard.formatDate` hardcodes `"en-US"` locale
|
||||
|
||||
**File:** `src/client/components/ThreadCard.tsx:21`
|
||||
**Issue:** `formatDate` calls `d.toLocaleDateString("en-US", ...)` with a hardcoded locale. Regardless of the user's language setting, thread card dates will always format in English (e.g. "Apr 17" not "17. Apr"). This is a locale bypass that survives even a correct i18next setup.
|
||||
**Fix:** Pass `undefined` (or the active i18n language) so the browser respects the user's locale:
|
||||
```ts
|
||||
function formatDate(iso: string): string {
|
||||
**File:** `src/client/components/ThreadCard.tsx:20`
|
||||
**Issue:** The `formatDate` function hardcodes `"en-US"` locale: `d.toLocaleDateString("en-US", { month: "short", day: "numeric" })`. This ignores the user's language preference and will always display English month abbreviations (e.g., "Apr 18" instead of "18. Apr." for German users).
|
||||
**Fix:** Accept and use the locale from `useLanguage()` or pass `undefined` to use the browser default:
|
||||
```tsx
|
||||
function formatDate(iso: string, locale?: string): string {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
||||
return d.toLocaleDateString(locale, { month: "short", day: "numeric" });
|
||||
}
|
||||
```
|
||||
Alternatively, import `i18n` from `../lib/i18n` and use `i18n.language` as the first argument.
|
||||
|
||||
### WR-07: `t` variable shadowed by filter callback parameter in `PlanningView`
|
||||
|
||||
**File:** `src/client/components/PlanningView.tsx:32`
|
||||
**Issue:** The file destructures `{ t }` from `useTranslation` at line 11, then on line 32 uses `t` as the name of the filter callback parameter:
|
||||
```ts
|
||||
const filteredThreads = (threads ?? [])
|
||||
.filter((t) => t.status === activeTab)
|
||||
.filter((t) => (categoryFilter ? t.categoryId === categoryFilter : true));
|
||||
// In the component:
|
||||
const { locale } = useFormatters();
|
||||
// ...
|
||||
{formatDate(createdAt, locale)}
|
||||
```
|
||||
The inner `t` shadows the outer translation function. While JS resolves this correctly inside the arrow functions, it makes the code misleading and will cause a lint warning (or error with strict shadowing rules). A future developer editing the filter body could accidentally call `t("some.key")` expecting a translation, and instead receive an object.
|
||||
**Fix:** Rename the filter parameter:
|
||||
```ts
|
||||
|
||||
### WR-02: Hardcoded English Placeholder Strings in Item Edit Form
|
||||
|
||||
**File:** `src/client/routes/items/$itemId.tsx:432-440`
|
||||
**Issue:** Two input placeholders are hardcoded English strings instead of using translation keys:
|
||||
- Line 432: `placeholder="Brand / Manufacturer (optional)"`
|
||||
- Line 440: `placeholder="Item name / Model"`
|
||||
|
||||
These will display in English regardless of the user's language setting.
|
||||
**Fix:** Add translation keys and use them:
|
||||
```tsx
|
||||
placeholder={t("collection:form.brandPlaceholder")}
|
||||
// ...
|
||||
placeholder={t("collection:form.modelPlaceholder")}
|
||||
```
|
||||
|
||||
### WR-03: Falsy Check on purchasePriceCents Discards Zero Values
|
||||
|
||||
**File:** `src/client/components/AddToCollectionModal.tsx:67`
|
||||
**Issue:** `purchasePriceCents || undefined` uses a falsy check. If a user enters a purchase price of `$0.00`, `purchasePriceCents` will be `0`, which is falsy. The value will be silently discarded and not sent to the API. While $0.00 purchase price is uncommon, it is valid (e.g., a gifted item).
|
||||
**Fix:** Use a nullish check instead:
|
||||
```tsx
|
||||
purchasePriceCents: purchasePriceCents ?? undefined,
|
||||
```
|
||||
Or keep the existing `purchasePrice` string check and only convert when present:
|
||||
```tsx
|
||||
purchasePriceCents: purchasePrice
|
||||
? Math.round(Number.parseFloat(purchasePrice) * 100)
|
||||
: undefined,
|
||||
```
|
||||
|
||||
## Info
|
||||
|
||||
### IN-01: Variable Shadowing of Translation Function `t`
|
||||
|
||||
**File:** `src/client/components/PlanningView.tsx:31-32`
|
||||
**Issue:** The `.filter()` callbacks use `t` as the parameter name, shadowing the `t` translation function from `useTranslation`. While this does not cause a runtime bug (the callbacks access object properties, not call the translation function), it is confusing and could lead to future bugs if someone tries to use translation inside the filter.
|
||||
**Fix:** Rename the filter parameter to a more descriptive name:
|
||||
```tsx
|
||||
const filteredThreads = (threads ?? [])
|
||||
.filter((thread) => thread.status === activeTab)
|
||||
.filter((thread) => (categoryFilter ? thread.categoryId === categoryFilter : true));
|
||||
```
|
||||
|
||||
---
|
||||
### IN-02: console.error Left in Production Code
|
||||
|
||||
## Info
|
||||
|
||||
### IN-01: Two hardcoded English strings in `ImageUpload` not extracted to locale
|
||||
|
||||
**File:** `src/client/components/ImageUpload.tsx:119,136`
|
||||
**Issue:** Line 119 has `alt="Item"` and line 136 has `title="Adjust framing"` as hardcoded English strings. The component already imports `useTranslation("common")` and uses it for other strings. These two are not in the EN locale file and will not be translated.
|
||||
**Fix:** Add keys to `en/common.json` (and `de/common.json`) under `imageUpload`:
|
||||
```json
|
||||
"imageUpload": {
|
||||
"clickToAdd": "Click to add photo",
|
||||
"invalidType": "...",
|
||||
"tooLarge": "...",
|
||||
"uploadFailed": "...",
|
||||
"altText": "Item photo",
|
||||
"adjustFraming": "Adjust framing"
|
||||
}
|
||||
```
|
||||
Then use them in the component:
|
||||
**File:** `src/client/routes/profile.tsx:331`
|
||||
**Issue:** `console.error("Account deletion failed:", err)` is left in the DangerZoneSection's error handler. While error logging can be useful, this appears to be a debug artifact since the error is not surfaced to the user.
|
||||
**Fix:** Either show the error to the user via a state message, or remove the console.error:
|
||||
```tsx
|
||||
// line 119
|
||||
alt={t("imageUpload.altText")}
|
||||
// line 136
|
||||
title={t("imageUpload.adjustFraming")}
|
||||
```
|
||||
|
||||
### IN-02: `CollectionTabs` uses inconsistent key depth for first two tabs
|
||||
|
||||
**File:** `src/client/components/ThreadTabs.tsx:13-15`
|
||||
**Issue:** The tabs array mixes key depths:
|
||||
```ts
|
||||
{ key: "gear", label: t("gear") },
|
||||
{ key: "planning", label: t("planning") },
|
||||
{ key: "setups", label: t("tabs.setups") },
|
||||
```
|
||||
`"gear"` and `"planning"` are looked up at the root of the `collection` namespace, while `"setups"` is nested under `"tabs"`. This asymmetry is accidental — if `"gear"` or `"planning"` ever gain sub-keys, lookups will break silently. Consistent nesting (all under `"tabs"`) is cleaner and mirrors standard i18n patterns.
|
||||
**Fix:** Move all three into a `tabs` grouping in the locale files and look them up uniformly:
|
||||
```ts
|
||||
{ key: "gear", label: t("tabs.gear") },
|
||||
{ key: "planning", label: t("tabs.planning") },
|
||||
{ key: "setups", label: t("tabs.setups") },
|
||||
```
|
||||
|
||||
### IN-03: `profile.tsx` silently swallows account-deletion error after logging to console
|
||||
|
||||
**File:** `src/client/routes/profile.tsx:327-332`
|
||||
**Issue:** `handleDelete` catches errors with `console.error` but provides no feedback to the user. If `deleteAccount.mutateAsync()` throws, the user sees nothing — no error message, no state change.
|
||||
**Fix:** Add error state and display an error message:
|
||||
```tsx
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
setDeleteError(null);
|
||||
try {
|
||||
await deleteAccount.mutateAsync();
|
||||
window.location.href = "/logout";
|
||||
} catch (err) {
|
||||
setDeleteError((err as Error).message || t("errors.somethingWentWrong"));
|
||||
}
|
||||
setMessage({ type: "error", text: (err as Error).message });
|
||||
}
|
||||
```
|
||||
Render `{deleteError && <p className="text-sm text-red-600">{deleteError}</p>}` in the danger zone form.
|
||||
|
||||
### IN-04: `de/threads.json` uses "Abgeschlossen" for `status.resolved` but `empty.noThreads` says "noch keine" vs EN "No threads found"
|
||||
### IN-03: Missing German Locale-Aware Date Formatting in PublicSetupCard
|
||||
|
||||
**File:** `src/client/locales/de/threads.json:42`
|
||||
**Issue:** The DE `empty.noThreads` reads "Noch keine Recherche-Threads" (meaning "No research threads yet") while the EN version reads "No threads found". These are used for two different states: the EN string is shown when a category filter returns no matches (not an "empty collection" state), but the DE string implies the collection is empty. When filtered to a category with no results, German users receive a misleading message.
|
||||
**Fix:** Align the DE translation with the EN intent:
|
||||
```json
|
||||
"empty": {
|
||||
"noThreads": "Keine Threads gefunden",
|
||||
"noCandidates": "Noch keine Kandidaten"
|
||||
}
|
||||
**File:** `src/client/components/PublicSetupCard.tsx:16-22`
|
||||
**Issue:** `toLocaleDateString(undefined, ...)` delegates to browser locale detection rather than the app's language setting. This means a German-language user on an English-locale browser will see English date formats. This is a minor inconsistency with the rest of the i18n implementation which explicitly passes locale.
|
||||
**Fix:** Use the `useLanguage()` hook to pass the app's language:
|
||||
```tsx
|
||||
const language = useLanguage();
|
||||
const formattedDate = new Date(setup.createdAt).toLocaleDateString(language, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
_Reviewed: 2026-04-17T00:00:00Z_
|
||||
_Reviewed: 2026-04-18T14:30:00Z_
|
||||
_Reviewer: Claude (gsd-code-reviewer)_
|
||||
_Depth: standard_
|
||||
|
||||
Reference in New Issue
Block a user