Add Anthropic API, test environment, remove locale settings

Feature 1: Anthropic API Integration
- Add @anthropic-ai/sdk with adapter wrapping OpenAI-shaped interface
- Support Claude models (opus, sonnet, haiku) with extended thinking
- Auto-reset model on provider switch, JSON retry logic
- Add Claude model pricing to ai-usage tracker
- Update AI settings form with Anthropic provider option

Feature 2: Remove Locale Settings UI
- Strip Localization tab from admin settings
- Remove i18n settings from router inferCategory and getFeatureFlags
- Keep franc document language detection intact

Feature 3: Test Environment with Role Impersonation
- Add isTest field to User, Program, Project, Competition models
- Test environment service: create/teardown with realistic dummy data
- JWT-based impersonation for test users (@test.local emails)
- Impersonation banner with quick-switch between test roles
- Test environment panel in admin settings (SUPER_ADMIN only)
- Email redirect: @test.local emails routed to admin with [TEST] prefix
- Complete data isolation: 45+ isTest:false filters across platform
  - All global queries on User/Project/Program/Competition
  - AI services blocked from processing test data
  - Cron jobs skip test rounds/users
  - Analytics/exports exclude test data
  - Admin layout/pickers hide test programs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 17:20:48 +01:00
parent f42b452899
commit 3e70de3a5a
55 changed files with 1630 additions and 770 deletions

View File

@@ -226,7 +226,8 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
{navigation.map((item) => {
const isActive =
pathname === item.href ||
(item.href !== '/admin' && pathname.startsWith(item.href))
(item.href !== '/admin' && pathname.startsWith(item.href)) ||
(item.href === '/admin/rounds' && pathname.startsWith('/admin/competitions'))
return (
<div key={item.name}>
<Link
@@ -258,12 +259,24 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
Administration
</p>
{dynamicAdminNav.map((item) => {
const isDisabled = item.name === 'Apply Page' && !currentEdition?.id
let isActive = pathname.startsWith(item.href)
if (item.activeMatch) {
isActive = pathname.includes(item.activeMatch)
} else if (item.activeExclude && pathname.includes(item.activeExclude)) {
isActive = false
}
if (isDisabled) {
return (
<span
key={item.name}
className="group flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium opacity-50 pointer-events-none text-muted-foreground"
>
<item.icon className="h-4 w-4 text-muted-foreground" />
{item.name}
</span>
)
}
return (
<Link
key={item.name}