Merge branch 'pr-125'

# Conflicts:
#	client/src/api/client.ts
#	client/src/i18n/translations/ar.ts
#	client/src/i18n/translations/es.ts
#	client/src/i18n/translations/fr.ts
#	client/src/i18n/translations/nl.ts
#	client/src/i18n/translations/ru.ts
#	client/src/i18n/translations/zh.ts
#	client/src/pages/AdminPage.tsx
#	client/src/pages/SettingsPage.tsx
#	server/package.json
#	server/src/db/migrations.ts
#	server/src/index.ts
#	server/src/routes/admin.ts
This commit is contained in:
Maurice
2026-03-30 23:10:34 +02:00
32 changed files with 3504 additions and 55 deletions

View File

@@ -61,6 +61,11 @@ export const authApi = {
changePassword: (data: { current_password: string; new_password: string }) => apiClient.put('/auth/me/password', data).then(r => r.data),
deleteOwnAccount: () => apiClient.delete('/auth/me').then(r => r.data),
demoLogin: () => apiClient.post('/auth/demo-login').then(r => r.data),
mcpTokens: {
list: () => apiClient.get('/auth/mcp-tokens').then(r => r.data),
create: (name: string) => apiClient.post('/auth/mcp-tokens', { name }).then(r => r.data),
delete: (id: number) => apiClient.delete(`/auth/mcp-tokens/${id}`).then(r => r.data),
},
}
export const tripsApi = {
@@ -170,6 +175,8 @@ export const adminApi = {
deleteInvite: (id: number) => apiClient.delete(`/admin/invites/${id}`).then(r => r.data),
auditLog: (params?: { limit?: number; offset?: number }) =>
apiClient.get('/admin/audit-log', { params }).then(r => r.data),
mcpTokens: () => apiClient.get('/admin/mcp-tokens').then(r => r.data),
deleteMcpToken: (id: number) => apiClient.delete(`/admin/mcp-tokens/${id}`).then(r => r.data),
}
export const addonsApi = {

View File

@@ -2,11 +2,12 @@ import { useEffect, useState } from 'react'
import { adminApi } from '../../api/client'
import { useTranslation } from '../../i18n'
import { useSettingsStore } from '../../store/settingsStore'
import { useAddonStore } from '../../store/addonStore'
import { useToast } from '../shared/Toast'
import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image } from 'lucide-react'
import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image, Terminal, Link2 } from 'lucide-react'
const ICON_MAP = {
ListChecks, Wallet, FileText, CalendarDays, Puzzle, Globe, Briefcase, Image,
ListChecks, Wallet, FileText, CalendarDays, Puzzle, Globe, Briefcase, Image, Terminal, Link2,
}
interface Addon {
@@ -32,6 +33,7 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking }
const dm = useSettingsStore(s => s.settings.dark_mode)
const dark = dm === true || dm === 'dark' || (dm === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)
const toast = useToast()
const refreshGlobalAddons = useAddonStore(s => s.loadAddons)
const [addons, setAddons] = useState([])
const [loading, setLoading] = useState(true)
@@ -57,7 +59,7 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking }
setAddons(prev => prev.map(a => a.id === addon.id ? { ...a, enabled: newEnabled } : a))
try {
await adminApi.updateAddon(addon.id, { enabled: newEnabled })
window.dispatchEvent(new Event('addons-changed'))
refreshGlobalAddons()
toast.success(t('admin.addons.toast.updated'))
} catch (err: unknown) {
// Rollback
@@ -68,6 +70,7 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking }
const tripAddons = addons.filter(a => a.type === 'trip')
const globalAddons = addons.filter(a => a.type === 'global')
const integrationAddons = addons.filter(a => a.type === 'integration')
if (loading) {
return (
@@ -144,6 +147,21 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking }
))}
</div>
)}
{/* Integration Addons */}
{integrationAddons.length > 0 && (
<div>
<div className="px-6 py-2.5 border-b border-t flex items-center gap-2" style={{ background: 'var(--bg-secondary)', borderColor: 'var(--border-secondary)' }}>
<Link2 size={13} style={{ color: 'var(--text-muted)' }} />
<span className="text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--text-muted)' }}>
{t('admin.addons.type.integration')} {t('admin.addons.integrationHint')}
</span>
</div>
{integrationAddons.map(addon => (
<AddonRow key={addon.id} addon={addon} onToggle={handleToggle} t={t} />
))}
</div>
)}
</div>
)}
</div>
@@ -188,11 +206,8 @@ function AddonRow({ addon, onToggle, t }: AddonRowProps) {
Coming Soon
</span>
)}
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full" style={{
background: addon.type === 'global' ? 'var(--bg-secondary)' : 'var(--bg-secondary)',
color: 'var(--text-muted)',
}}>
{addon.type === 'global' ? t('admin.addons.type.global') : t('admin.addons.type.trip')}
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full" style={{ background: 'var(--bg-secondary)', color: 'var(--text-muted)' }}>
{addon.type === 'global' ? t('admin.addons.type.global') : addon.type === 'integration' ? t('admin.addons.type.integration') : t('admin.addons.type.trip')}
</span>
</div>
<p className="text-xs mt-0.5" style={{ color: 'var(--text-muted)' }}>{label.description}</p>

View File

@@ -0,0 +1,120 @@
import { useState, useEffect } from 'react'
import { adminApi } from '../../api/client'
import { useToast } from '../shared/Toast'
import { Key, Trash2, User, Loader2 } from 'lucide-react'
import { useTranslation } from '../../i18n'
interface AdminMcpToken {
id: number
name: string
token_prefix: string
created_at: string
last_used_at: string | null
user_id: number
username: string
}
export default function AdminMcpTokensPanel() {
const [tokens, setTokens] = useState<AdminMcpToken[]>([])
const [isLoading, setIsLoading] = useState(true)
const [deleteConfirmId, setDeleteConfirmId] = useState<number | null>(null)
const toast = useToast()
const { t, locale } = useTranslation()
useEffect(() => {
setIsLoading(true)
adminApi.mcpTokens()
.then(d => setTokens(d.tokens || []))
.catch(() => toast.error(t('admin.mcpTokens.loadError')))
.finally(() => setIsLoading(false))
}, [])
const handleDelete = async (id: number) => {
try {
await adminApi.deleteMcpToken(id)
setTokens(prev => prev.filter(tk => tk.id !== id))
setDeleteConfirmId(null)
toast.success(t('admin.mcpTokens.deleteSuccess'))
} catch {
toast.error(t('admin.mcpTokens.deleteError'))
}
}
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold" style={{ color: 'var(--text-primary)' }}>{t('admin.mcpTokens.title')}</h2>
<p className="text-sm mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{t('admin.mcpTokens.subtitle')}</p>
</div>
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-card)' }}>
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-5 h-5 animate-spin" style={{ color: 'var(--text-tertiary)' }} />
</div>
) : tokens.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 gap-2">
<Key className="w-8 h-8" style={{ color: 'var(--text-tertiary)' }} />
<p className="text-sm" style={{ color: 'var(--text-tertiary)' }}>{t('admin.mcpTokens.empty')}</p>
</div>
) : (
<>
<div className="grid grid-cols-[1fr_auto_auto_auto_auto] gap-x-4 px-4 py-2.5 text-xs font-medium border-b"
style={{ color: 'var(--text-tertiary)', borderColor: 'var(--border-primary)', background: 'var(--bg-secondary)' }}>
<span>{t('admin.mcpTokens.tokenName')}</span>
<span>{t('admin.mcpTokens.owner')}</span>
<span className="text-right">{t('admin.mcpTokens.created')}</span>
<span className="text-right">{t('admin.mcpTokens.lastUsed')}</span>
<span></span>
</div>
{tokens.map((token, i) => (
<div key={token.id}
className="grid grid-cols-[1fr_auto_auto_auto_auto] items-center gap-x-4 px-4 py-3"
style={{ borderBottom: i < tokens.length - 1 ? '1px solid var(--border-primary)' : undefined }}>
<div className="min-w-0">
<p className="text-sm font-medium truncate" style={{ color: 'var(--text-primary)' }}>{token.name}</p>
<p className="text-xs font-mono mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{token.token_prefix}...</p>
</div>
<div className="flex items-center gap-1.5 text-sm" style={{ color: 'var(--text-secondary)' }}>
<User className="w-3.5 h-3.5 flex-shrink-0" />
<span className="whitespace-nowrap">{token.username}</span>
</div>
<span className="text-xs whitespace-nowrap text-right" style={{ color: 'var(--text-tertiary)' }}>
{new Date(token.created_at).toLocaleDateString(locale)}
</span>
<span className="text-xs whitespace-nowrap text-right" style={{ color: 'var(--text-tertiary)' }}>
{token.last_used_at ? new Date(token.last_used_at).toLocaleDateString(locale) : t('admin.mcpTokens.never')}
</span>
<button onClick={() => setDeleteConfirmId(token.id)}
className="p-1.5 rounded-lg transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
style={{ color: 'var(--text-tertiary)' }} title={t('common.delete')}>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</>
)}
</div>
{deleteConfirmId !== null && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }}
onClick={e => { if (e.target === e.currentTarget) setDeleteConfirmId(null) }}>
<div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4" style={{ background: 'var(--bg-card)' }}>
<h3 className="text-base font-semibold" style={{ color: 'var(--text-primary)' }}>{t('admin.mcpTokens.deleteTitle')}</h3>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>{t('admin.mcpTokens.deleteMessage')}</p>
<div className="flex gap-2 justify-end">
<button onClick={() => setDeleteConfirmId(null)}
className="px-4 py-2 rounded-lg text-sm border" style={{ borderColor: 'var(--border-primary)', color: 'var(--text-secondary)' }}>
{t('common.cancel')}
</button>
<button onClick={() => handleDelete(deleteConfirmId)}
className="px-4 py-2 rounded-lg text-sm font-medium text-white bg-red-600 hover:bg-red-700">
{t('common.delete')}
</button>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -3,8 +3,8 @@ import ReactDOM from 'react-dom'
import { Link, useNavigate, useLocation } from 'react-router-dom'
import { useAuthStore } from '../../store/authStore'
import { useSettingsStore } from '../../store/settingsStore'
import { useAddonStore } from '../../store/addonStore'
import { useTranslation } from '../../i18n'
import { addonsApi } from '../../api/client'
import { Plane, LogOut, Settings, ChevronDown, Shield, ArrowLeft, Users, Moon, Sun, Monitor, CalendarDays, Briefcase, Globe } from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
@@ -28,29 +28,21 @@ interface Addon {
export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }: NavbarProps): React.ReactElement {
const { user, logout } = useAuthStore()
const { settings, updateSetting } = useSettingsStore()
const { addons: allAddons, loadAddons } = useAddonStore()
const { t, locale } = useTranslation()
const navigate = useNavigate()
const location = useLocation()
const [userMenuOpen, setUserMenuOpen] = useState<boolean>(false)
const [appVersion, setAppVersion] = useState<string | null>(null)
const [globalAddons, setGlobalAddons] = useState<Addon[]>([])
const darkMode = settings.dark_mode
const dark = darkMode === true || darkMode === 'dark' || (darkMode === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)
const loadAddons = () => {
if (user) {
addonsApi.enabled().then(data => {
setGlobalAddons(data.addons.filter(a => a.type === 'global'))
}).catch(() => {})
}
}
useEffect(loadAddons, [user, location.pathname])
// Listen for addon changes from AddonManager
// Only show 'global' type addons in the navbar — 'integration' addons have no dedicated page
const globalAddons = allAddons.filter((a: Addon) => a.type === 'global' && a.enabled)
useEffect(() => {
const handler = () => loadAddons()
window.addEventListener('addons-changed', handler)
return () => window.removeEventListener('addons-changed', handler)
}, [user])
if (user) loadAddons()
}, [user, location.pathname])
useEffect(() => {
import('../../api/client').then(({ authApi }) => {

View File

@@ -190,6 +190,31 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'share.permCollab': 'الدردشة',
'settings.on': 'تشغيل',
'settings.off': 'إيقاف',
'settings.mcp.title': 'إعداد MCP',
'settings.mcp.endpoint': 'نقطة نهاية MCP',
'settings.mcp.clientConfig': 'إعداد العميل',
'settings.mcp.clientConfigHint': 'استبدل <your_token> برمز API من القائمة أدناه. قد يحتاج مسار npx إلى ضبط وفق نظامك (مثلاً C:\\PROGRA~1\\nodejs\\npx.cmd على Windows).',
'settings.mcp.copy': 'نسخ',
'settings.mcp.copied': 'تم النسخ!',
'settings.mcp.apiTokens': 'رموز API',
'settings.mcp.createToken': 'إنشاء رمز جديد',
'settings.mcp.noTokens': 'لا توجد رموز بعد. أنشئ رمزاً للاتصال بعملاء MCP.',
'settings.mcp.tokenCreatedAt': 'أُنشئ',
'settings.mcp.tokenUsedAt': 'استُخدم',
'settings.mcp.deleteTokenTitle': 'حذف الرمز',
'settings.mcp.deleteTokenMessage': 'سيتوقف هذا الرمز عن العمل فوراً. أي عميل MCP يستخدمه سيفقد الوصول.',
'settings.mcp.modal.createTitle': 'إنشاء رمز API',
'settings.mcp.modal.tokenName': 'اسم الرمز',
'settings.mcp.modal.tokenNamePlaceholder': 'مثال: Claude Desktop، حاسوب العمل',
'settings.mcp.modal.creating': 'جارٍ الإنشاء…',
'settings.mcp.modal.create': 'إنشاء الرمز',
'settings.mcp.modal.createdTitle': 'تم إنشاء الرمز',
'settings.mcp.modal.createdWarning': 'سيُعرض هذا الرمز مرة واحدة فقط. انسخه واحفظه الآن — لا يمكن استرداده.',
'settings.mcp.modal.done': 'تم',
'settings.mcp.toast.created': 'تم إنشاء الرمز',
'settings.mcp.toast.createError': 'فشل إنشاء الرمز',
'settings.mcp.toast.deleted': 'تم حذف الرمز',
'settings.mcp.toast.deleteError': 'فشل حذف الرمز',
'settings.account': 'الحساب',
'settings.username': 'اسم المستخدم',
'settings.email': 'البريد الإلكتروني',
@@ -325,6 +350,20 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'admin.tabs.config': 'الإعدادات',
'admin.tabs.templates': 'قوالب التعبئة',
'admin.tabs.addons': 'الإضافات',
'admin.tabs.mcpTokens': 'رموز MCP',
'admin.mcpTokens.title': 'رموز MCP',
'admin.mcpTokens.subtitle': 'إدارة رموز API لجميع المستخدمين',
'admin.mcpTokens.owner': 'المالك',
'admin.mcpTokens.tokenName': 'اسم الرمز',
'admin.mcpTokens.created': 'تاريخ الإنشاء',
'admin.mcpTokens.lastUsed': 'آخر استخدام',
'admin.mcpTokens.never': 'أبداً',
'admin.mcpTokens.empty': 'لم يتم إنشاء أي رموز MCP بعد',
'admin.mcpTokens.deleteTitle': 'حذف الرمز',
'admin.mcpTokens.deleteMessage': 'سيتم إلغاء هذا الرمز فوراً. سيفقد المستخدم وصوله إلى MCP عبر هذا الرمز.',
'admin.mcpTokens.deleteSuccess': 'تم حذف الرمز',
'admin.mcpTokens.deleteError': 'فشل حذف الرمز',
'admin.mcpTokens.loadError': 'فشل تحميل الرموز',
'admin.tabs.github': 'GitHub',
'admin.stats.users': 'المستخدمون',
'admin.stats.trips': 'الرحلات',
@@ -425,8 +464,10 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
// Addons
'admin.addons.title': 'الإضافات',
'admin.addons.subtitle': 'فعّل أو عطّل الميزات لتخصيص تجربة TREK.',
'admin.addons.catalog.memories.name': 'ذكريات',
'admin.addons.catalog.memories.description': 'ألبومات صور مشتركة لكل رحلة',
'admin.addons.catalog.memories.name': 'صور (Immich)',
'admin.addons.catalog.memories.description': 'شارك صور رحلتك عبر Immich',
'admin.addons.catalog.mcp.name': 'MCP',
'admin.addons.catalog.mcp.description': 'بروتوكول سياق النموذج لتكامل مساعد الذكاء الاصطناعي',
'admin.addons.catalog.packing.name': 'التعبئة',
'admin.addons.catalog.packing.description': 'قوائم تحقق لإعداد أمتعتك لكل رحلة',
'admin.addons.catalog.budget.name': 'الميزانية',
@@ -445,8 +486,10 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'admin.addons.disabled': 'معطّل',
'admin.addons.type.trip': 'رحلة',
'admin.addons.type.global': 'عام',
'admin.addons.type.integration': 'تكامل',
'admin.addons.tripHint': 'متاح كعلامة تبويب داخل كل رحلة',
'admin.addons.globalHint': 'متاح كقسم مستقل في التنقل الرئيسي',
'admin.addons.integrationHint': 'خدمات الواجهة الخلفية وتكاملات API بدون صفحة مخصصة',
'admin.addons.toast.updated': 'تم تحديث الإضافة',
'admin.addons.toast.error': 'فشل تحديث الإضافة',
'admin.addons.noAddons': 'لا توجد إضافات متاحة',

View File

@@ -235,6 +235,31 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'settings.mfa.toastEnabled': 'Autenticação em duas etapas ativada',
'settings.mfa.toastDisabled': 'Autenticação em duas etapas desativada',
'settings.mfa.demoBlocked': 'Indisponível no modo demonstração',
'settings.mcp.title': 'Configuração MCP',
'settings.mcp.endpoint': 'Endpoint MCP',
'settings.mcp.clientConfig': 'Configuração do cliente',
'settings.mcp.clientConfigHint': 'Substitua <your_token> por um token de API da lista abaixo. O caminho para o npx pode precisar ser ajustado para o seu sistema (ex.: C:\\PROGRA~1\\nodejs\\npx.cmd no Windows).',
'settings.mcp.copy': 'Copiar',
'settings.mcp.copied': 'Copiado!',
'settings.mcp.apiTokens': 'Tokens de API',
'settings.mcp.createToken': 'Criar novo token',
'settings.mcp.noTokens': 'Nenhum token ainda. Crie um para conectar clientes MCP.',
'settings.mcp.tokenCreatedAt': 'Criado em',
'settings.mcp.tokenUsedAt': 'Usado em',
'settings.mcp.deleteTokenTitle': 'Excluir token',
'settings.mcp.deleteTokenMessage': 'Este token deixará de funcionar imediatamente. Qualquer cliente MCP que o utilize perderá o acesso.',
'settings.mcp.modal.createTitle': 'Criar token de API',
'settings.mcp.modal.tokenName': 'Nome do token',
'settings.mcp.modal.tokenNamePlaceholder': 'ex.: Claude Desktop, Notebook do trabalho',
'settings.mcp.modal.creating': 'Criando…',
'settings.mcp.modal.create': 'Criar token',
'settings.mcp.modal.createdTitle': 'Token criado',
'settings.mcp.modal.createdWarning': 'Este token será exibido apenas uma vez. Copie e guarde agora — não poderá ser recuperado.',
'settings.mcp.modal.done': 'Concluído',
'settings.mcp.toast.created': 'Token criado',
'settings.mcp.toast.createError': 'Falha ao criar token',
'settings.mcp.toast.deleted': 'Token excluído',
'settings.mcp.toast.deleteError': 'Falha ao excluir token',
// Login
'login.error': 'Falha no login. Verifique suas credenciais.',
@@ -432,6 +457,8 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'admin.addons.catalog.atlas.description': 'Mapa mundial com países visitados e estatísticas',
'admin.addons.catalog.collab.name': 'Colab',
'admin.addons.catalog.collab.description': 'Notas, enquetes e chat em tempo real para planejar a viagem',
'admin.addons.catalog.mcp.name': 'MCP',
'admin.addons.catalog.mcp.description': 'Model Context Protocol para integração com assistentes de IA',
'admin.addons.subtitleBefore': 'Ative ou desative recursos para personalizar sua ',
'admin.addons.subtitleAfter': ' experiência.',
'admin.addons.enabled': 'Ativado',

View File

@@ -185,6 +185,31 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'share.permCollab': 'Chat',
'settings.on': 'An',
'settings.off': 'Aus',
'settings.mcp.title': 'MCP-Konfiguration',
'settings.mcp.endpoint': 'MCP-Endpunkt',
'settings.mcp.clientConfig': 'Client-Konfiguration',
'settings.mcp.clientConfigHint': 'Ersetze <your_token> durch ein API-Token aus der Liste unten. Der Pfad zu npx muss ggf. für dein System angepasst werden (z. B. C:\\PROGRA~1\\nodejs\\npx.cmd unter Windows).',
'settings.mcp.copy': 'Kopieren',
'settings.mcp.copied': 'Kopiert!',
'settings.mcp.apiTokens': 'API-Tokens',
'settings.mcp.createToken': 'Neuen Token erstellen',
'settings.mcp.noTokens': 'Noch keine Tokens. Erstelle einen, um MCP-Clients zu verbinden.',
'settings.mcp.tokenCreatedAt': 'Erstellt',
'settings.mcp.tokenUsedAt': 'Verwendet',
'settings.mcp.deleteTokenTitle': 'Token löschen',
'settings.mcp.deleteTokenMessage': 'Dieser Token wird sofort ungültig. Jeder MCP-Client, der ihn verwendet, verliert den Zugang.',
'settings.mcp.modal.createTitle': 'API-Token erstellen',
'settings.mcp.modal.tokenName': 'Token-Name',
'settings.mcp.modal.tokenNamePlaceholder': 'z. B. Claude Desktop, Arbeits-Laptop',
'settings.mcp.modal.creating': 'Wird erstellt…',
'settings.mcp.modal.create': 'Token erstellen',
'settings.mcp.modal.createdTitle': 'Token erstellt',
'settings.mcp.modal.createdWarning': 'Dieser Token wird nur einmal angezeigt. Kopiere und speichere ihn jetzt — er kann nicht wiederhergestellt werden.',
'settings.mcp.modal.done': 'Fertig',
'settings.mcp.toast.created': 'Token erstellt',
'settings.mcp.toast.createError': 'Token konnte nicht erstellt werden',
'settings.mcp.toast.deleted': 'Token gelöscht',
'settings.mcp.toast.deleteError': 'Token konnte nicht gelöscht werden',
'settings.account': 'Konto',
'settings.username': 'Benutzername',
'settings.email': 'E-Mail',
@@ -433,14 +458,18 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'admin.addons.catalog.collab.description': 'Echtzeit-Notizen, Umfragen und Chat für die Reiseplanung',
'admin.addons.catalog.memories.name': 'Fotos (Immich)',
'admin.addons.catalog.memories.description': 'Reisefotos über deine Immich-Instanz teilen',
'admin.addons.catalog.mcp.name': 'MCP',
'admin.addons.catalog.mcp.description': 'Model Context Protocol für die KI-Assistenten-Integration',
'admin.addons.subtitleBefore': 'Aktiviere oder deaktiviere Funktionen, um ',
'admin.addons.subtitleAfter': ' nach deinen Wünschen anzupassen.',
'admin.addons.enabled': 'Aktiviert',
'admin.addons.disabled': 'Deaktiviert',
'admin.addons.type.trip': 'Trip',
'admin.addons.type.global': 'Global',
'admin.addons.type.integration': 'Integration',
'admin.addons.tripHint': 'Verfügbar als Tab innerhalb jedes Trips',
'admin.addons.globalHint': 'Verfügbar als eigenständiger Bereich in der Navigation',
'admin.addons.integrationHint': 'Backend-Dienste und API-Integrationen ohne eigene Seite',
'admin.addons.toast.updated': 'Addon aktualisiert',
'admin.addons.toast.error': 'Addon konnte nicht aktualisiert werden',
'admin.addons.noAddons': 'Keine Addons verfügbar',
@@ -456,6 +485,22 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'admin.weather.requestsDesc': 'Kostenlos, kein API-Schlüssel erforderlich',
'admin.weather.locationHint': 'Das Wetter wird anhand des ersten Ortes mit Koordinaten im jeweiligen Tag berechnet. Ist kein Ort am Tag eingeplant, wird ein beliebiger Ort aus der Ortsliste als Referenz verwendet.',
// MCP Tokens
'admin.tabs.mcpTokens': 'MCP-Tokens',
'admin.mcpTokens.title': 'MCP-Tokens',
'admin.mcpTokens.subtitle': 'API-Tokens aller Benutzer verwalten',
'admin.mcpTokens.owner': 'Besitzer',
'admin.mcpTokens.tokenName': 'Token-Name',
'admin.mcpTokens.created': 'Erstellt',
'admin.mcpTokens.lastUsed': 'Zuletzt verwendet',
'admin.mcpTokens.never': 'Nie',
'admin.mcpTokens.empty': 'Es wurden noch keine MCP-Tokens erstellt',
'admin.mcpTokens.deleteTitle': 'Token löschen',
'admin.mcpTokens.deleteMessage': 'Dieser Token wird sofort widerrufen. Der Benutzer verliert den MCP-Zugang über diesen Token.',
'admin.mcpTokens.deleteSuccess': 'Token gelöscht',
'admin.mcpTokens.deleteError': 'Token konnte nicht gelöscht werden',
'admin.mcpTokens.loadError': 'Tokens konnten nicht geladen werden',
// GitHub
'admin.tabs.github': 'GitHub',

View File

@@ -185,6 +185,31 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'share.permCollab': 'Chat',
'settings.on': 'On',
'settings.off': 'Off',
'settings.mcp.title': 'MCP Configuration',
'settings.mcp.endpoint': 'MCP Endpoint',
'settings.mcp.clientConfig': 'Client Configuration',
'settings.mcp.clientConfigHint': 'Replace <your_token> with an API token from the list below. The path to npx may need to be adjusted for your system (e.g. C:\\PROGRA~1\\nodejs\\npx.cmd on Windows).',
'settings.mcp.copy': 'Copy',
'settings.mcp.copied': 'Copied!',
'settings.mcp.apiTokens': 'API Tokens',
'settings.mcp.createToken': 'Create New Token',
'settings.mcp.noTokens': 'No tokens yet. Create one to connect MCP clients.',
'settings.mcp.tokenCreatedAt': 'Created',
'settings.mcp.tokenUsedAt': 'Used',
'settings.mcp.deleteTokenTitle': 'Delete Token',
'settings.mcp.deleteTokenMessage': 'This token will stop working immediately. Any MCP client using it will lose access.',
'settings.mcp.modal.createTitle': 'Create API Token',
'settings.mcp.modal.tokenName': 'Token Name',
'settings.mcp.modal.tokenNamePlaceholder': 'e.g. Claude Desktop, Work laptop',
'settings.mcp.modal.creating': 'Creating…',
'settings.mcp.modal.create': 'Create Token',
'settings.mcp.modal.createdTitle': 'Token Created',
'settings.mcp.modal.createdWarning': 'This token will only be shown once. Copy and store it now — it cannot be recovered.',
'settings.mcp.modal.done': 'Done',
'settings.mcp.toast.created': 'Token created',
'settings.mcp.toast.createError': 'Failed to create token',
'settings.mcp.toast.deleted': 'Token deleted',
'settings.mcp.toast.deleteError': 'Failed to delete token',
'settings.account': 'Account',
'settings.username': 'Username',
'settings.email': 'Email',
@@ -433,14 +458,18 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'admin.addons.catalog.collab.description': 'Real-time notes, polls, and chat for trip planning',
'admin.addons.catalog.memories.name': 'Photos (Immich)',
'admin.addons.catalog.memories.description': 'Share trip photos via your Immich instance',
'admin.addons.catalog.mcp.name': 'MCP',
'admin.addons.catalog.mcp.description': 'Model Context Protocol for AI assistant integration',
'admin.addons.subtitleBefore': 'Enable or disable features to customize your ',
'admin.addons.subtitleAfter': ' experience.',
'admin.addons.enabled': 'Enabled',
'admin.addons.disabled': 'Disabled',
'admin.addons.type.trip': 'Trip',
'admin.addons.type.global': 'Global',
'admin.addons.type.integration': 'Integration',
'admin.addons.tripHint': 'Available as a tab within each trip',
'admin.addons.globalHint': 'Available as a standalone section in the main navigation',
'admin.addons.integrationHint': 'Backend services and API integrations with no dedicated page',
'admin.addons.toast.updated': 'Addon updated',
'admin.addons.toast.error': 'Failed to update addon',
'admin.addons.noAddons': 'No addons available',
@@ -457,6 +486,20 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'admin.weather.locationHint': 'Weather is based on the first place with coordinates in each day. If no place is assigned to a day, any place from the place list is used as a reference.',
// GitHub
'admin.tabs.mcpTokens': 'MCP Tokens',
'admin.mcpTokens.title': 'MCP Tokens',
'admin.mcpTokens.subtitle': 'Manage API tokens across all users',
'admin.mcpTokens.owner': 'Owner',
'admin.mcpTokens.tokenName': 'Token Name',
'admin.mcpTokens.created': 'Created',
'admin.mcpTokens.lastUsed': 'Last Used',
'admin.mcpTokens.never': 'Never',
'admin.mcpTokens.empty': 'No MCP tokens have been created yet',
'admin.mcpTokens.deleteTitle': 'Delete Token',
'admin.mcpTokens.deleteMessage': 'This will revoke the token immediately. The user will lose MCP access through this token.',
'admin.mcpTokens.deleteSuccess': 'Token deleted',
'admin.mcpTokens.deleteError': 'Failed to delete token',
'admin.mcpTokens.loadError': 'Failed to load tokens',
'admin.tabs.github': 'GitHub',
'admin.audit.subtitle': 'Security-sensitive and administration events (backups, users, MFA, settings).',

View File

@@ -186,6 +186,31 @@ const es: Record<string, string> = {
'share.permCollab': 'Chat',
'settings.on': 'Activado',
'settings.off': 'Desactivado',
'settings.mcp.title': 'Configuración MCP',
'settings.mcp.endpoint': 'Endpoint MCP',
'settings.mcp.clientConfig': 'Configuración del cliente',
'settings.mcp.clientConfigHint': 'Reemplaza <your_token> con un token de la lista de abajo. Es posible que debas ajustar la ruta de npx según tu sistema (p. ej. C:\\PROGRA~1\\nodejs\\npx.cmd en Windows).',
'settings.mcp.copy': 'Copiar',
'settings.mcp.copied': '¡Copiado!',
'settings.mcp.apiTokens': 'Tokens de API',
'settings.mcp.createToken': 'Crear nuevo token',
'settings.mcp.noTokens': 'Sin tokens aún. Crea uno para conectar clientes MCP.',
'settings.mcp.tokenCreatedAt': 'Creado',
'settings.mcp.tokenUsedAt': 'Usado',
'settings.mcp.deleteTokenTitle': 'Eliminar token',
'settings.mcp.deleteTokenMessage': 'Este token dejará de funcionar de inmediato. Cualquier cliente MCP que lo use perderá el acceso.',
'settings.mcp.modal.createTitle': 'Crear token de API',
'settings.mcp.modal.tokenName': 'Nombre del token',
'settings.mcp.modal.tokenNamePlaceholder': 'p. ej. Claude Desktop, Portátil de trabajo',
'settings.mcp.modal.creating': 'Creando…',
'settings.mcp.modal.create': 'Crear token',
'settings.mcp.modal.createdTitle': 'Token creado',
'settings.mcp.modal.createdWarning': 'Este token solo se mostrará una vez. Cópialo y guárdalo ahora — no se podrá recuperar.',
'settings.mcp.modal.done': 'Listo',
'settings.mcp.toast.created': 'Token creado',
'settings.mcp.toast.createError': 'Error al crear el token',
'settings.mcp.toast.deleted': 'Token eliminado',
'settings.mcp.toast.deleteError': 'Error al eliminar el token',
'settings.account': 'Cuenta',
'settings.username': 'Usuario',
'settings.email': 'Correo',
@@ -420,8 +445,10 @@ const es: Record<string, string> = {
'admin.addons.disabled': 'Desactivado',
'admin.addons.type.trip': 'Viaje',
'admin.addons.type.global': 'Global',
'admin.addons.type.integration': 'Integración',
'admin.addons.tripHint': 'Disponible como pestaña dentro de cada viaje',
'admin.addons.globalHint': 'Disponible como sección independiente en la navegación principal',
'admin.addons.integrationHint': 'Servicios backend e integraciones de API sin página dedicada',
'admin.addons.toast.updated': 'Complemento actualizado',
'admin.addons.toast.error': 'No se pudo actualizar el complemento',
'admin.addons.noAddons': 'No hay complementos disponibles',
@@ -436,6 +463,22 @@ const es: Record<string, string> = {
'admin.weather.requestsDesc': 'Gratis, sin necesidad de clave API',
'admin.weather.locationHint': 'El tiempo se basa en el primer lugar con coordenadas de cada día. Si no hay ningún lugar asignado a un día, se usa como referencia cualquier lugar de la lista.',
// MCP Tokens
'admin.tabs.mcpTokens': 'Tokens MCP',
'admin.mcpTokens.title': 'Tokens MCP',
'admin.mcpTokens.subtitle': 'Gestionar tokens de API de todos los usuarios',
'admin.mcpTokens.owner': 'Propietario',
'admin.mcpTokens.tokenName': 'Nombre del token',
'admin.mcpTokens.created': 'Creado',
'admin.mcpTokens.lastUsed': 'Último uso',
'admin.mcpTokens.never': 'Nunca',
'admin.mcpTokens.empty': 'Aún no se han creado tokens MCP',
'admin.mcpTokens.deleteTitle': 'Eliminar token',
'admin.mcpTokens.deleteMessage': 'Este token se revocará inmediatamente. El usuario perderá el acceso MCP a través de este token.',
'admin.mcpTokens.deleteSuccess': 'Token eliminado',
'admin.mcpTokens.deleteError': 'No se pudo eliminar el token',
'admin.mcpTokens.loadError': 'No se pudieron cargar los tokens',
// GitHub
'admin.tabs.github': 'GitHub',
@@ -1062,8 +1105,10 @@ const es: Record<string, string> = {
'photos.linkPlace': 'Vincular lugar',
'photos.noPlace': 'Sin lugar',
'photos.uploadN': 'Subida de {n} foto(s)',
'admin.addons.catalog.memories.name': 'Recuerdos',
'admin.addons.catalog.memories.description': 'Álbumes de fotos compartidos para cada viaje',
'admin.addons.catalog.memories.name': 'Fotos (Immich)',
'admin.addons.catalog.memories.description': 'Comparte fotos de viaje a través de tu instancia de Immich',
'admin.addons.catalog.mcp.name': 'MCP',
'admin.addons.catalog.mcp.description': 'Protocolo de contexto de modelo para integración con asistentes de IA',
'admin.addons.catalog.packing.name': 'Equipaje',
'admin.addons.catalog.packing.description': 'Prepara tu equipaje con listas de comprobación para cada viaje',
'admin.addons.catalog.budget.name': 'Presupuesto',

View File

@@ -185,6 +185,31 @@ const fr: Record<string, string> = {
'share.permCollab': 'Chat',
'settings.on': 'Activé',
'settings.off': 'Désactivé',
'settings.mcp.title': 'Configuration MCP',
'settings.mcp.endpoint': 'Point de terminaison MCP',
'settings.mcp.clientConfig': 'Configuration du client',
'settings.mcp.clientConfigHint': 'Remplacez <your_token> par un token API de la liste ci-dessous. Le chemin vers npx devra peut-être être ajusté selon votre système (ex. C:\\PROGRA~1\\nodejs\\npx.cmd sous Windows).',
'settings.mcp.copy': 'Copier',
'settings.mcp.copied': 'Copié !',
'settings.mcp.apiTokens': 'Tokens API',
'settings.mcp.createToken': 'Créer un token',
'settings.mcp.noTokens': 'Aucun token pour l\'instant. Créez-en un pour connecter des clients MCP.',
'settings.mcp.tokenCreatedAt': 'Créé',
'settings.mcp.tokenUsedAt': 'Utilisé',
'settings.mcp.deleteTokenTitle': 'Supprimer le token',
'settings.mcp.deleteTokenMessage': 'Ce token cessera de fonctionner immédiatement. Tout client MCP l\'utilisant perdra l\'accès.',
'settings.mcp.modal.createTitle': 'Créer un token API',
'settings.mcp.modal.tokenName': 'Nom du token',
'settings.mcp.modal.tokenNamePlaceholder': 'ex. Claude Desktop, Ordinateur pro',
'settings.mcp.modal.creating': 'Création…',
'settings.mcp.modal.create': 'Créer le token',
'settings.mcp.modal.createdTitle': 'Token créé',
'settings.mcp.modal.createdWarning': 'Ce token ne sera affiché qu\'une seule fois. Copiez-le et conservez-le maintenant — il ne pourra pas être récupéré.',
'settings.mcp.modal.done': 'Terminé',
'settings.mcp.toast.created': 'Token créé',
'settings.mcp.toast.createError': 'Impossible de créer le token',
'settings.mcp.toast.deleted': 'Token supprimé',
'settings.mcp.toast.deleteError': 'Impossible de supprimer le token',
'settings.account': 'Compte',
'settings.username': 'Nom d\'utilisateur',
'settings.email': 'E-mail',
@@ -417,8 +442,10 @@ const fr: Record<string, string> = {
'admin.tabs.addons': 'Extensions',
'admin.addons.title': 'Extensions',
'admin.addons.subtitle': 'Activez ou désactivez des fonctionnalités pour personnaliser votre expérience TREK.',
'admin.addons.catalog.memories.name': 'Souvenirs',
'admin.addons.catalog.memories.description': 'Albums photo partagés pour chaque voyage',
'admin.addons.catalog.memories.name': 'Photos (Immich)',
'admin.addons.catalog.memories.description': 'Partagez vos photos de voyage via votre instance Immich',
'admin.addons.catalog.mcp.name': 'MCP',
'admin.addons.catalog.mcp.description': 'Protocole de contexte de modèle pour l\'intégration d\'assistants IA',
'admin.addons.catalog.packing.name': 'Bagages',
'admin.addons.catalog.packing.description': 'Listes de contrôle pour préparer vos bagages pour chaque voyage',
'admin.addons.catalog.budget.name': 'Budget',
@@ -437,8 +464,10 @@ const fr: Record<string, string> = {
'admin.addons.disabled': 'Désactivé',
'admin.addons.type.trip': 'Voyage',
'admin.addons.type.global': 'Global',
'admin.addons.type.integration': 'Intégration',
'admin.addons.tripHint': 'Disponible comme onglet dans chaque voyage',
'admin.addons.globalHint': 'Disponible comme section autonome dans la navigation principale',
'admin.addons.integrationHint': 'Services backend et intégrations API sans page dédiée',
'admin.addons.toast.updated': 'Extension mise à jour',
'admin.addons.toast.error': 'Échec de la mise à jour de l\'extension',
'admin.addons.noAddons': 'Aucune extension disponible',
@@ -468,6 +497,22 @@ const fr: Record<string, string> = {
'admin.audit.col.ip': 'IP',
'admin.audit.col.details': 'Détails',
// MCP Tokens
'admin.tabs.mcpTokens': 'Tokens MCP',
'admin.mcpTokens.title': 'Tokens MCP',
'admin.mcpTokens.subtitle': 'Gérer les tokens API de tous les utilisateurs',
'admin.mcpTokens.owner': 'Propriétaire',
'admin.mcpTokens.tokenName': 'Nom du token',
'admin.mcpTokens.created': 'Créé',
'admin.mcpTokens.lastUsed': 'Dernière utilisation',
'admin.mcpTokens.never': 'Jamais',
'admin.mcpTokens.empty': 'Aucun token MCP n\'a encore été créé',
'admin.mcpTokens.deleteTitle': 'Supprimer le token',
'admin.mcpTokens.deleteMessage': 'Ce token sera révoqué immédiatement. L\'utilisateur perdra l\'accès MCP via ce token.',
'admin.mcpTokens.deleteSuccess': 'Token supprimé',
'admin.mcpTokens.deleteError': 'Impossible de supprimer le token',
'admin.mcpTokens.loadError': 'Impossible de charger les tokens',
// GitHub
'admin.tabs.github': 'GitHub',
'admin.github.title': 'Historique des versions',

View File

@@ -185,6 +185,31 @@ const nl: Record<string, string> = {
'share.permCollab': 'Chat',
'settings.on': 'Aan',
'settings.off': 'Uit',
'settings.mcp.title': 'MCP-configuratie',
'settings.mcp.endpoint': 'MCP-eindpunt',
'settings.mcp.clientConfig': 'Clientconfiguratie',
'settings.mcp.clientConfigHint': 'Vervang <your_token> door een API-token uit de onderstaande lijst. Het pad naar npx moet mogelijk worden aangepast voor jouw systeem (bijv. C:\\PROGRA~1\\nodejs\\npx.cmd op Windows).',
'settings.mcp.copy': 'Kopiëren',
'settings.mcp.copied': 'Gekopieerd!',
'settings.mcp.apiTokens': 'API-tokens',
'settings.mcp.createToken': 'Nieuw token aanmaken',
'settings.mcp.noTokens': 'Nog geen tokens. Maak er een aan om MCP-clients te verbinden.',
'settings.mcp.tokenCreatedAt': 'Aangemaakt',
'settings.mcp.tokenUsedAt': 'Gebruikt',
'settings.mcp.deleteTokenTitle': 'Token verwijderen',
'settings.mcp.deleteTokenMessage': 'Dit token werkt onmiddellijk niet meer. Elke MCP-client die het gebruikt verliest de toegang.',
'settings.mcp.modal.createTitle': 'API-token aanmaken',
'settings.mcp.modal.tokenName': 'Tokennaam',
'settings.mcp.modal.tokenNamePlaceholder': 'bijv. Claude Desktop, Werklaptop',
'settings.mcp.modal.creating': 'Aanmaken…',
'settings.mcp.modal.create': 'Token aanmaken',
'settings.mcp.modal.createdTitle': 'Token aangemaakt',
'settings.mcp.modal.createdWarning': 'Dit token wordt slechts één keer getoond. Kopieer en bewaar het nu — het kan niet worden hersteld.',
'settings.mcp.modal.done': 'Klaar',
'settings.mcp.toast.created': 'Token aangemaakt',
'settings.mcp.toast.createError': 'Token aanmaken mislukt',
'settings.mcp.toast.deleted': 'Token verwijderd',
'settings.mcp.toast.deleteError': 'Token verwijderen mislukt',
'settings.account': 'Account',
'settings.username': 'Gebruikersnaam',
'settings.email': 'E-mail',
@@ -418,8 +443,10 @@ const nl: Record<string, string> = {
'admin.tabs.addons': 'Add-ons',
'admin.addons.title': 'Add-ons',
'admin.addons.subtitle': 'Schakel functies in of uit om je TREK-ervaring aan te passen.',
'admin.addons.catalog.memories.name': 'Herinneringen',
'admin.addons.catalog.memories.description': 'Gedeelde fotoalbums voor elke reis',
'admin.addons.catalog.memories.name': 'Foto\'s (Immich)',
'admin.addons.catalog.memories.description': 'Deel reisfoto\'s via je Immich-instantie',
'admin.addons.catalog.mcp.name': 'MCP',
'admin.addons.catalog.mcp.description': 'Model Context Protocol voor AI-assistent integratie',
'admin.addons.catalog.packing.name': 'Inpakken',
'admin.addons.catalog.packing.description': 'Checklists om je bagage voor elke reis voor te bereiden',
'admin.addons.catalog.budget.name': 'Budget',
@@ -438,8 +465,10 @@ const nl: Record<string, string> = {
'admin.addons.disabled': 'Uitgeschakeld',
'admin.addons.type.trip': 'Reis',
'admin.addons.type.global': 'Globaal',
'admin.addons.type.integration': 'Integratie',
'admin.addons.tripHint': 'Beschikbaar als tabblad binnen elke reis',
'admin.addons.globalHint': 'Beschikbaar als zelfstandig onderdeel in de hoofdnavigatie',
'admin.addons.integrationHint': 'Backenddiensten en API-integraties zonder eigen pagina',
'admin.addons.toast.updated': 'Add-on bijgewerkt',
'admin.addons.toast.error': 'Add-on bijwerken mislukt',
'admin.addons.noAddons': 'Geen add-ons beschikbaar',
@@ -455,6 +484,22 @@ const nl: Record<string, string> = {
'admin.weather.requestsDesc': 'Gratis, geen API-sleutel vereist',
'admin.weather.locationHint': 'Het weer is gebaseerd op de eerste plaats met coördinaten op elke dag. Als er geen plaats aan een dag is toegewezen, wordt een plaats uit de lijst als referentie gebruikt.',
// MCP Tokens
'admin.tabs.mcpTokens': 'MCP-tokens',
'admin.mcpTokens.title': 'MCP-tokens',
'admin.mcpTokens.subtitle': 'API-tokens van alle gebruikers beheren',
'admin.mcpTokens.owner': 'Eigenaar',
'admin.mcpTokens.tokenName': 'Tokennaam',
'admin.mcpTokens.created': 'Aangemaakt',
'admin.mcpTokens.lastUsed': 'Laatst gebruikt',
'admin.mcpTokens.never': 'Nooit',
'admin.mcpTokens.empty': 'Er zijn nog geen MCP-tokens aangemaakt',
'admin.mcpTokens.deleteTitle': 'Token verwijderen',
'admin.mcpTokens.deleteMessage': 'Dit token wordt onmiddellijk ingetrokken. De gebruiker verliest MCP-toegang via dit token.',
'admin.mcpTokens.deleteSuccess': 'Token verwijderd',
'admin.mcpTokens.deleteError': 'Token kon niet worden verwijderd',
'admin.mcpTokens.loadError': 'Tokens konden niet worden geladen',
// GitHub
'admin.tabs.github': 'GitHub',

View File

@@ -185,6 +185,31 @@ const ru: Record<string, string> = {
'share.permCollab': 'Чат',
'settings.on': 'Вкл.',
'settings.off': 'Выкл.',
'settings.mcp.title': 'Настройка MCP',
'settings.mcp.endpoint': 'MCP-эндпоинт',
'settings.mcp.clientConfig': 'Конфигурация клиента',
'settings.mcp.clientConfigHint': 'Замените <your_token> на API-токен из списка ниже. Путь к npx может потребовать настройки для вашей системы (например, C:\\PROGRA~1\\nodejs\\npx.cmd в Windows).',
'settings.mcp.copy': 'Копировать',
'settings.mcp.copied': 'Скопировано!',
'settings.mcp.apiTokens': 'API-токены',
'settings.mcp.createToken': 'Создать токен',
'settings.mcp.noTokens': 'Токенов пока нет. Создайте один для подключения MCP-клиентов.',
'settings.mcp.tokenCreatedAt': 'Создан',
'settings.mcp.tokenUsedAt': 'Использован',
'settings.mcp.deleteTokenTitle': 'Удалить токен',
'settings.mcp.deleteTokenMessage': 'Этот токен перестанет работать немедленно. Любой MCP-клиент, использующий его, потеряет доступ.',
'settings.mcp.modal.createTitle': 'Создать API-токен',
'settings.mcp.modal.tokenName': 'Название токена',
'settings.mcp.modal.tokenNamePlaceholder': 'напр. Claude Desktop, Рабочий ноутбук',
'settings.mcp.modal.creating': 'Создание…',
'settings.mcp.modal.create': 'Создать токен',
'settings.mcp.modal.createdTitle': 'Токен создан',
'settings.mcp.modal.createdWarning': 'Этот токен будет показан только один раз. Скопируйте и сохраните его сейчас — восстановить его будет невозможно.',
'settings.mcp.modal.done': 'Готово',
'settings.mcp.toast.created': 'Токен создан',
'settings.mcp.toast.createError': 'Не удалось создать токен',
'settings.mcp.toast.deleted': 'Токен удалён',
'settings.mcp.toast.deleteError': 'Не удалось удалить токен',
'settings.account': 'Аккаунт',
'settings.username': 'Имя пользователя',
'settings.email': 'Эл. почта',
@@ -418,8 +443,10 @@ const ru: Record<string, string> = {
'admin.tabs.addons': 'Дополнения',
'admin.addons.title': 'Дополнения',
'admin.addons.subtitle': 'Включайте или отключайте функции для настройки TREK под себя.',
'admin.addons.catalog.memories.name': 'Воспоминания',
'admin.addons.catalog.memories.description': 'Общие фотоальбомы для каждой поездки',
'admin.addons.catalog.memories.name': 'Фото (Immich)',
'admin.addons.catalog.memories.description': 'Делитесь фотографиями из поездок через Immich',
'admin.addons.catalog.mcp.name': 'MCP',
'admin.addons.catalog.mcp.description': 'Протокол контекста модели для интеграции с ИИ-ассистентами',
'admin.addons.catalog.packing.name': 'Сборы',
'admin.addons.catalog.packing.description': 'Чек-листы для подготовки багажа к каждой поездке',
'admin.addons.catalog.budget.name': 'Бюджет',
@@ -438,8 +465,10 @@ const ru: Record<string, string> = {
'admin.addons.disabled': 'Отключено',
'admin.addons.type.trip': 'Поездка',
'admin.addons.type.global': 'Глобально',
'admin.addons.type.integration': 'Интеграция',
'admin.addons.tripHint': 'Доступно как вкладка внутри каждой поездки',
'admin.addons.globalHint': 'Доступно как отдельный раздел в основной навигации',
'admin.addons.integrationHint': 'Фоновые сервисы и API-интеграции без отдельной страницы',
'admin.addons.toast.updated': 'Дополнение обновлено',
'admin.addons.toast.error': 'Не удалось обновить дополнение',
'admin.addons.noAddons': 'Нет доступных дополнений',
@@ -455,6 +484,22 @@ const ru: Record<string, string> = {
'admin.weather.requestsDesc': 'Бесплатно, API-ключ не требуется',
'admin.weather.locationHint': 'Погода основана на первом месте с координатами в каждом дне. Если ни одно место не назначено на день, в качестве ориентира используется любое место из списка.',
// MCP Tokens
'admin.tabs.mcpTokens': 'MCP-токены',
'admin.mcpTokens.title': 'MCP-токены',
'admin.mcpTokens.subtitle': 'Управление API-токенами всех пользователей',
'admin.mcpTokens.owner': 'Владелец',
'admin.mcpTokens.tokenName': 'Название токена',
'admin.mcpTokens.created': 'Создан',
'admin.mcpTokens.lastUsed': 'Последнее использование',
'admin.mcpTokens.never': 'Никогда',
'admin.mcpTokens.empty': 'MCP-токены ещё не созданы',
'admin.mcpTokens.deleteTitle': 'Удалить токен',
'admin.mcpTokens.deleteMessage': 'Токен будет немедленно отозван. Пользователь потеряет доступ к MCP через этот токен.',
'admin.mcpTokens.deleteSuccess': 'Токен удалён',
'admin.mcpTokens.deleteError': 'Не удалось удалить токен',
'admin.mcpTokens.loadError': 'Не удалось загрузить токены',
// GitHub
'admin.tabs.github': 'GitHub',

View File

@@ -185,6 +185,31 @@ const zh: Record<string, string> = {
'share.permCollab': '聊天',
'settings.on': '开',
'settings.off': '关',
'settings.mcp.title': 'MCP 配置',
'settings.mcp.endpoint': 'MCP 端点',
'settings.mcp.clientConfig': '客户端配置',
'settings.mcp.clientConfigHint': '将 <your_token> 替换为下方列表中的 API 令牌。npx 的路径可能需要根据您的系统进行调整(例如 Windows 上为 C:\\PROGRA~1\\nodejs\\npx.cmd。',
'settings.mcp.copy': '复制',
'settings.mcp.copied': '已复制!',
'settings.mcp.apiTokens': 'API 令牌',
'settings.mcp.createToken': '创建新令牌',
'settings.mcp.noTokens': '暂无令牌,请创建一个以连接 MCP 客户端。',
'settings.mcp.tokenCreatedAt': '创建于',
'settings.mcp.tokenUsedAt': '使用于',
'settings.mcp.deleteTokenTitle': '删除令牌',
'settings.mcp.deleteTokenMessage': '此令牌将立即失效,使用它的所有 MCP 客户端将失去访问权限。',
'settings.mcp.modal.createTitle': '创建 API 令牌',
'settings.mcp.modal.tokenName': '令牌名称',
'settings.mcp.modal.tokenNamePlaceholder': '例如Claude Desktop、工作电脑',
'settings.mcp.modal.creating': '创建中…',
'settings.mcp.modal.create': '创建令牌',
'settings.mcp.modal.createdTitle': '令牌已创建',
'settings.mcp.modal.createdWarning': '此令牌只会显示一次,请立即复制并妥善保存——无法找回。',
'settings.mcp.modal.done': '完成',
'settings.mcp.toast.created': '令牌已创建',
'settings.mcp.toast.createError': '创建令牌失败',
'settings.mcp.toast.deleted': '令牌已删除',
'settings.mcp.toast.deleteError': '删除令牌失败',
'settings.account': '账户',
'settings.username': '用户名',
'settings.email': '邮箱',
@@ -418,8 +443,10 @@ const zh: Record<string, string> = {
'admin.tabs.addons': '扩展',
'admin.addons.title': '扩展',
'admin.addons.subtitle': '启用或禁用功能以自定义你的 TREK 体验。',
'admin.addons.catalog.memories.name': '回忆',
'admin.addons.catalog.memories.description': '每次旅行的共享相册',
'admin.addons.catalog.memories.name': '照片 (Immich)',
'admin.addons.catalog.memories.description': '通过 Immich 实例分享旅行照片',
'admin.addons.catalog.mcp.name': 'MCP',
'admin.addons.catalog.mcp.description': '用于 AI 助手集成的模型上下文协议',
'admin.addons.catalog.packing.name': '行李',
'admin.addons.catalog.packing.description': '每次旅行的行李准备清单',
'admin.addons.catalog.budget.name': '预算',
@@ -438,8 +465,10 @@ const zh: Record<string, string> = {
'admin.addons.disabled': '已禁用',
'admin.addons.type.trip': '旅行',
'admin.addons.type.global': '全局',
'admin.addons.type.integration': '集成',
'admin.addons.tripHint': '在每次旅行中作为标签页显示',
'admin.addons.globalHint': '在主导航中作为独立板块显示',
'admin.addons.integrationHint': '后端服务和 API 集成,无专属页面',
'admin.addons.toast.updated': '扩展已更新',
'admin.addons.toast.error': '更新扩展失败',
'admin.addons.noAddons': '暂无可用扩展',
@@ -455,6 +484,22 @@ const zh: Record<string, string> = {
'admin.weather.requestsDesc': '免费,无需 API 密钥',
'admin.weather.locationHint': '天气基于每天中第一个有坐标的地点。如果当天没有分配地点,则使用地点列表中的任意地点作为参考。',
// MCP Tokens
'admin.tabs.mcpTokens': 'MCP 令牌',
'admin.mcpTokens.title': 'MCP 令牌',
'admin.mcpTokens.subtitle': '管理所有用户的 API 令牌',
'admin.mcpTokens.owner': '所有者',
'admin.mcpTokens.tokenName': '令牌名称',
'admin.mcpTokens.created': '创建时间',
'admin.mcpTokens.lastUsed': '最后使用',
'admin.mcpTokens.never': '从未',
'admin.mcpTokens.empty': '尚未创建任何 MCP 令牌',
'admin.mcpTokens.deleteTitle': '删除令牌',
'admin.mcpTokens.deleteMessage': '此令牌将立即被撤销。用户将失去通过此令牌的 MCP 访问权限。',
'admin.mcpTokens.deleteSuccess': '令牌已删除',
'admin.mcpTokens.deleteError': '删除令牌失败',
'admin.mcpTokens.loadError': '加载令牌失败',
// GitHub
'admin.tabs.github': 'GitHub',

View File

@@ -14,6 +14,7 @@ import GitHubPanel from '../components/Admin/GitHubPanel'
import AddonManager from '../components/Admin/AddonManager'
import PackingTemplateManager from '../components/Admin/PackingTemplateManager'
import AuditLogPanel from '../components/Admin/AuditLogPanel'
import AdminMcpTokensPanel from '../components/Admin/AdminMcpTokensPanel'
import { Users, Map, Briefcase, Shield, Trash2, Edit2, Camera, FileText, Eye, EyeOff, Save, CheckCircle, XCircle, Loader2, UserPlus, ArrowUpCircle, ExternalLink, Download, AlertTriangle, RefreshCw, GitBranch, Sun, Link2, Copy, Plus } from 'lucide-react'
import CustomSelect from '../components/shared/CustomSelect'
@@ -63,6 +64,7 @@ export default function AdminPage(): React.ReactElement {
{ id: 'settings', label: t('admin.tabs.settings') },
{ id: 'backup', label: t('admin.tabs.backup') },
{ id: 'audit', label: t('admin.tabs.audit') },
{ id: 'mcp-tokens', label: t('admin.tabs.mcpTokens') },
{ id: 'github', label: t('admin.tabs.github') },
]
@@ -997,6 +999,8 @@ export default function AdminPage(): React.ReactElement {
{activeTab === 'audit' && <AuditLogPanel />}
{activeTab === 'mcp-tokens' && <AdminMcpTokensPanel />}
{activeTab === 'github' && <GitHubPanel />}
</div>
</div>

View File

@@ -6,9 +6,10 @@ import { SUPPORTED_LANGUAGES, useTranslation } from '../i18n'
import Navbar from '../components/Layout/Navbar'
import CustomSelect from '../components/shared/CustomSelect'
import { useToast } from '../components/shared/Toast'
import { Save, Map, Palette, User, Moon, Sun, Monitor, Shield, Camera, Trash2, Lock, KeyRound } from 'lucide-react'
import { Save, Map, Palette, User, Moon, Sun, Monitor, Shield, Camera, Trash2, Lock, KeyRound, Terminal, Copy, Plus, Check } from 'lucide-react'
import { authApi, adminApi, notificationsApi } from '../api/client'
import apiClient from '../api/client'
import { useAddonStore } from '../store/addonStore'
import type { LucideIcon } from 'lucide-react'
import type { UserWithOidc } from '../types'
import { getApiErrorMessage } from '../types'
@@ -18,6 +19,14 @@ interface MapPreset {
url: string
}
interface McpToken {
id: number
name: string
token_prefix: string
created_at: string
last_used_at: string | null
}
const MAP_PRESETS: MapPreset[] = [
{ name: 'OpenStreetMap', url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' },
{ name: 'OpenStreetMap DE', url: 'https://tile.openstreetmap.de/{z}/{x}/{y}.png' },
@@ -105,32 +114,34 @@ export default function SettingsPage(): React.ReactElement {
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean | 'blocked'>(false)
const avatarInputRef = React.useRef<HTMLInputElement>(null)
const { settings, updateSetting, updateSettings } = useSettingsStore()
const { isEnabled: addonEnabled, loadAddons } = useAddonStore()
const { t, locale } = useTranslation()
const toast = useToast()
const navigate = useNavigate()
const [saving, setSaving] = useState<Record<string, boolean>>({})
// Immich
const [memoriesEnabled, setMemoriesEnabled] = useState(false)
// Addon gating (derived from store)
const memoriesEnabled = addonEnabled('memories')
const mcpEnabled = addonEnabled('mcp')
const [immichUrl, setImmichUrl] = useState('')
const [immichApiKey, setImmichApiKey] = useState('')
const [immichConnected, setImmichConnected] = useState(false)
const [immichTesting, setImmichTesting] = useState(false)
useEffect(() => {
apiClient.get('/addons').then(r => {
const mem = r.data.addons?.find((a: any) => a.id === 'memories' && a.enabled)
setMemoriesEnabled(!!mem)
if (mem) {
apiClient.get('/integrations/immich/settings').then(r2 => {
setImmichUrl(r2.data.immich_url || '')
setImmichConnected(r2.data.connected)
}).catch(() => {})
}
}).catch(() => {})
loadAddons()
}, [])
useEffect(() => {
if (memoriesEnabled) {
apiClient.get('/integrations/immich/settings').then(r2 => {
setImmichUrl(r2.data.immich_url || '')
setImmichConnected(r2.data.connected)
}).catch(() => {})
}
}, [memoriesEnabled])
const handleSaveImmich = async () => {
setSaving(s => ({ ...s, immich: true }))
try {
@@ -164,6 +175,67 @@ export default function SettingsPage(): React.ReactElement {
}
}
// MCP tokens
const [mcpTokens, setMcpTokens] = useState<McpToken[]>([])
const [mcpModalOpen, setMcpModalOpen] = useState(false)
const [mcpNewName, setMcpNewName] = useState('')
const [mcpCreatedToken, setMcpCreatedToken] = useState<string | null>(null)
const [mcpCreating, setMcpCreating] = useState(false)
const [mcpDeleteId, setMcpDeleteId] = useState<number | null>(null)
const [copiedKey, setCopiedKey] = useState<string | null>(null)
useEffect(() => {
authApi.mcpTokens.list().then(d => setMcpTokens(d.tokens || [])).catch(() => {})
}, [])
const handleCreateMcpToken = async () => {
if (!mcpNewName.trim()) return
setMcpCreating(true)
try {
const d = await authApi.mcpTokens.create(mcpNewName.trim())
setMcpCreatedToken(d.token.raw_token)
setMcpNewName('')
setMcpTokens(prev => [{ id: d.token.id, name: d.token.name, token_prefix: d.token.token_prefix, created_at: d.token.created_at, last_used_at: null }, ...prev])
} catch {
toast.error(t('settings.mcp.toast.createError'))
} finally {
setMcpCreating(false)
}
}
const handleDeleteMcpToken = async (id: number) => {
try {
await authApi.mcpTokens.delete(id)
setMcpTokens(prev => prev.filter(tk => tk.id !== id))
setMcpDeleteId(null)
toast.success(t('settings.mcp.toast.deleted'))
} catch {
toast.error(t('settings.mcp.toast.deleteError'))
}
}
const handleCopy = (text: string, key: string) => {
navigator.clipboard.writeText(text).then(() => {
setCopiedKey(key)
setTimeout(() => setCopiedKey(null), 2000)
})
}
const mcpEndpoint = `${window.location.origin}/mcp`
const mcpJsonConfig = `{
"mcpServers": {
"trek": {
"command": "npx",
"args": [
"mcp-remote",
"${mcpEndpoint}",
"--header",
"Authorization: Bearer <your_token>"
]
}
}
}`
// Map settings
const [mapTileUrl, setMapTileUrl] = useState<string>(settings.map_tile_url || '')
const [defaultLat, setDefaultLat] = useState<number | string>(settings.default_lat || 48.8566)
@@ -572,6 +644,162 @@ export default function SettingsPage(): React.ReactElement {
</Section>
)}
{/* MCP Configuration — only when MCP addon is enabled */}
{mcpEnabled && <Section title={t('settings.mcp.title')} icon={Terminal}>
{/* Endpoint URL */}
<div>
<label className="block text-sm font-medium mb-1.5" style={{ color: 'var(--text-secondary)' }}>{t('settings.mcp.endpoint')}</label>
<div className="flex items-center gap-2">
<code className="flex-1 px-3 py-2 rounded-lg text-sm font-mono border" style={{ background: 'var(--bg-secondary)', borderColor: 'var(--border-primary)', color: 'var(--text-primary)' }}>
{mcpEndpoint}
</code>
<button onClick={() => handleCopy(mcpEndpoint, 'endpoint')}
className="p-2 rounded-lg border transition-colors hover:bg-slate-100 dark:hover:bg-slate-700"
style={{ borderColor: 'var(--border-primary)' }} title={t('settings.mcp.copy')}>
{copiedKey === 'endpoint' ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" style={{ color: 'var(--text-secondary)' }} />}
</button>
</div>
</div>
{/* JSON config box */}
<div>
<div className="flex items-center justify-between mb-1.5">
<label className="block text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>{t('settings.mcp.clientConfig')}</label>
<button onClick={() => handleCopy(mcpJsonConfig, 'json')}
className="flex items-center gap-1.5 px-2.5 py-1 rounded text-xs border transition-colors hover:bg-slate-100 dark:hover:bg-slate-700"
style={{ borderColor: 'var(--border-primary)', color: 'var(--text-secondary)' }}>
{copiedKey === 'json' ? <Check className="w-3 h-3 text-green-500" /> : <Copy className="w-3 h-3" />}
{copiedKey === 'json' ? t('settings.mcp.copied') : t('settings.mcp.copy')}
</button>
</div>
<pre className="p-3 rounded-lg text-xs font-mono overflow-x-auto border" style={{ background: 'var(--bg-secondary)', borderColor: 'var(--border-primary)', color: 'var(--text-primary)' }}>
{mcpJsonConfig}
</pre>
<p className="mt-1.5 text-xs" style={{ color: 'var(--text-tertiary)' }}>{t('settings.mcp.clientConfigHint')}</p>
</div>
{/* Token list */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>{t('settings.mcp.apiTokens')}</label>
<button onClick={() => { setMcpModalOpen(true); setMcpCreatedToken(null); setMcpNewName('') }}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
style={{ background: 'var(--accent-primary, #4f46e5)', color: '#fff' }}>
<Plus className="w-3.5 h-3.5" /> {t('settings.mcp.createToken')}
</button>
</div>
{mcpTokens.length === 0 ? (
<p className="text-sm py-3 text-center rounded-lg border" style={{ color: 'var(--text-tertiary)', borderColor: 'var(--border-primary)' }}>
{t('settings.mcp.noTokens')}
</p>
) : (
<div className="rounded-lg border overflow-hidden" style={{ borderColor: 'var(--border-primary)' }}>
{mcpTokens.map((token, i) => (
<div key={token.id} className="flex items-center gap-3 px-4 py-3"
style={{ borderBottom: i < mcpTokens.length - 1 ? '1px solid var(--border-primary)' : undefined }}>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate" style={{ color: 'var(--text-primary)' }}>{token.name}</p>
<p className="text-xs font-mono mt-0.5" style={{ color: 'var(--text-tertiary)' }}>
{token.token_prefix}...
<span className="ml-3 font-sans">{t('settings.mcp.tokenCreatedAt')} {new Date(token.created_at).toLocaleDateString(locale)}</span>
{token.last_used_at && (
<span className="ml-2">· {t('settings.mcp.tokenUsedAt')} {new Date(token.last_used_at).toLocaleDateString(locale)}</span>
)}
</p>
</div>
<button onClick={() => setMcpDeleteId(token.id)}
className="p-1.5 rounded-lg transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
style={{ color: 'var(--text-tertiary)' }} title={t('settings.mcp.deleteTokenTitle')}>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
)}
</div>
</Section>}
{/* Create MCP Token modal */}
{mcpModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }}
onClick={e => { if (e.target === e.currentTarget && !mcpCreatedToken) { setMcpModalOpen(false) } }}>
<div className="rounded-xl shadow-xl w-full max-w-md p-6 space-y-4" style={{ background: 'var(--bg-card)' }}>
{!mcpCreatedToken ? (
<>
<h3 className="text-lg font-semibold" style={{ color: 'var(--text-primary)' }}>{t('settings.mcp.modal.createTitle')}</h3>
<div>
<label className="block text-sm font-medium mb-1.5" style={{ color: 'var(--text-secondary)' }}>{t('settings.mcp.modal.tokenName')}</label>
<input type="text" value={mcpNewName} onChange={e => setMcpNewName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleCreateMcpToken()}
placeholder={t('settings.mcp.modal.tokenNamePlaceholder')}
className="w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-300"
style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-secondary)', color: 'var(--text-primary)' }}
autoFocus />
</div>
<div className="flex gap-2 justify-end pt-1">
<button onClick={() => setMcpModalOpen(false)}
className="px-4 py-2 rounded-lg text-sm border" style={{ borderColor: 'var(--border-primary)', color: 'var(--text-secondary)' }}>
{t('common.cancel')}
</button>
<button onClick={handleCreateMcpToken} disabled={!mcpNewName.trim() || mcpCreating}
className="px-4 py-2 rounded-lg text-sm font-medium text-white disabled:opacity-50"
style={{ background: 'var(--accent-primary, #4f46e5)' }}>
{mcpCreating ? t('settings.mcp.modal.creating') : t('settings.mcp.modal.create')}
</button>
</div>
</>
) : (
<>
<h3 className="text-lg font-semibold" style={{ color: 'var(--text-primary)' }}>{t('settings.mcp.modal.createdTitle')}</h3>
<div className="flex items-start gap-2 p-3 rounded-lg border border-amber-200" style={{ background: 'rgba(251,191,36,0.1)' }}>
<span className="text-amber-500 mt-0.5"></span>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>{t('settings.mcp.modal.createdWarning')}</p>
</div>
<div className="relative">
<pre className="p-3 pr-10 rounded-lg text-xs font-mono break-all border whitespace-pre-wrap" style={{ background: 'var(--bg-secondary)', borderColor: 'var(--border-primary)', color: 'var(--text-primary)' }}>
{mcpCreatedToken}
</pre>
<button onClick={() => handleCopy(mcpCreatedToken, 'new-token')}
className="absolute top-2 right-2 p-1.5 rounded transition-colors hover:bg-slate-200 dark:hover:bg-slate-600"
style={{ color: 'var(--text-secondary)' }} title={t('settings.mcp.copy')}>
{copiedKey === 'new-token' ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
</button>
</div>
<div className="flex justify-end">
<button onClick={() => { setMcpModalOpen(false); setMcpCreatedToken(null) }}
className="px-4 py-2 rounded-lg text-sm font-medium text-white"
style={{ background: 'var(--accent-primary, #4f46e5)' }}>
{t('settings.mcp.modal.done')}
</button>
</div>
</>
)}
</div>
</div>
)}
{/* Delete MCP Token confirm */}
{mcpDeleteId !== null && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }}
onClick={e => { if (e.target === e.currentTarget) setMcpDeleteId(null) }}>
<div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4" style={{ background: 'var(--bg-card)' }}>
<h3 className="text-base font-semibold" style={{ color: 'var(--text-primary)' }}>{t('settings.mcp.deleteTokenTitle')}</h3>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>{t('settings.mcp.deleteTokenMessage')}</p>
<div className="flex gap-2 justify-end">
<button onClick={() => setMcpDeleteId(null)}
className="px-4 py-2 rounded-lg text-sm border" style={{ borderColor: 'var(--border-primary)', color: 'var(--text-secondary)' }}>
{t('common.cancel')}
</button>
<button onClick={() => handleDeleteMcpToken(mcpDeleteId)}
className="px-4 py-2 rounded-lg text-sm font-medium text-white bg-red-600 hover:bg-red-700">
{t('settings.mcp.deleteTokenTitle')}
</button>
</div>
</div>
</div>
)}
{/* Account */}
<Section title={t('settings.account')} icon={User}>
<div>

View File

@@ -0,0 +1,35 @@
import { create } from 'zustand'
import { addonsApi } from '../api/client'
interface Addon {
id: string
name: string
type: string
icon: string
enabled: boolean
}
interface AddonState {
addons: Addon[]
loaded: boolean
loadAddons: () => Promise<void>
isEnabled: (id: string) => boolean
}
export const useAddonStore = create<AddonState>((set, get) => ({
addons: [],
loaded: false,
loadAddons: async () => {
try {
const data = await addonsApi.enabled()
set({ addons: data.addons || [], loaded: true })
} catch {
set({ loaded: true })
}
},
isEnabled: (id: string) => {
return get().addons.some(a => a.id === id && a.enabled)
},
}))

View File

@@ -11,7 +11,7 @@ export default defineConfig({
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
globPatterns: ['**/*.{js,css,html,svg,png,woff,woff2,ttf}'],
navigateFallback: 'index.html',
navigateFallbackDenylist: [/^\/api/, /^\/uploads/],
navigateFallbackDenylist: [/^\/api/, /^\/uploads/, /^\/mcp/],
runtimeCaching: [
{
// Carto map tiles (default provider)
@@ -101,6 +101,10 @@ export default defineConfig({
'/ws': {
target: 'http://localhost:3001',
ws: true,
},
'/mcp': {
target: 'http://localhost:3001',
changeOrigin: true,
}
}
}