ideaco 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/.dockerignore +33 -0
  2. package/.nvmrc +1 -0
  3. package/ARCHITECTURE.md +394 -0
  4. package/Dockerfile +50 -0
  5. package/LICENSE +29 -0
  6. package/README.md +206 -0
  7. package/bin/i18n.js +46 -0
  8. package/bin/ideaco.js +494 -0
  9. package/deploy.sh +15 -0
  10. package/docker-compose.yml +30 -0
  11. package/electron/main.cjs +986 -0
  12. package/electron/preload.cjs +14 -0
  13. package/electron/web-backends.cjs +854 -0
  14. package/jsconfig.json +8 -0
  15. package/next.config.mjs +34 -0
  16. package/package.json +134 -0
  17. package/postcss.config.mjs +6 -0
  18. package/public/demo/dashboard.png +0 -0
  19. package/public/demo/employee.png +0 -0
  20. package/public/demo/messages.png +0 -0
  21. package/public/demo/office.png +0 -0
  22. package/public/demo/requirement.png +0 -0
  23. package/public/logo.jpeg +0 -0
  24. package/public/logo.png +0 -0
  25. package/scripts/prepare-electron.js +67 -0
  26. package/scripts/release.js +76 -0
  27. package/src/app/api/agents/[agentId]/chat/route.js +70 -0
  28. package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
  29. package/src/app/api/agents/[agentId]/route.js +106 -0
  30. package/src/app/api/avatar/route.js +104 -0
  31. package/src/app/api/browse-dir/route.js +44 -0
  32. package/src/app/api/chat/route.js +265 -0
  33. package/src/app/api/company/factory-reset/route.js +43 -0
  34. package/src/app/api/company/route.js +82 -0
  35. package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
  36. package/src/app/api/departments/route.js +92 -0
  37. package/src/app/api/group-chat-loop/events/route.js +70 -0
  38. package/src/app/api/group-chat-loop/route.js +94 -0
  39. package/src/app/api/mailbox/route.js +100 -0
  40. package/src/app/api/messages/route.js +14 -0
  41. package/src/app/api/providers/[id]/configure/route.js +21 -0
  42. package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
  43. package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
  44. package/src/app/api/providers/route.js +11 -0
  45. package/src/app/api/requirements/route.js +242 -0
  46. package/src/app/api/secretary/route.js +65 -0
  47. package/src/app/api/system/cli-backends/route.js +91 -0
  48. package/src/app/api/system/cron/route.js +110 -0
  49. package/src/app/api/system/knowledge/route.js +104 -0
  50. package/src/app/api/system/plugins/route.js +40 -0
  51. package/src/app/api/system/skills/route.js +46 -0
  52. package/src/app/api/system/status/route.js +46 -0
  53. package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
  54. package/src/app/api/talent-market/[profileId]/route.js +17 -0
  55. package/src/app/api/talent-market/route.js +26 -0
  56. package/src/app/api/teams/route.js +773 -0
  57. package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
  58. package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
  59. package/src/app/globals.css +130 -0
  60. package/src/app/layout.jsx +40 -0
  61. package/src/app/page.jsx +97 -0
  62. package/src/components/AgentChatModal.jsx +164 -0
  63. package/src/components/AgentDetailModal.jsx +425 -0
  64. package/src/components/AgentSpyModal.jsx +481 -0
  65. package/src/components/AvatarGrid.jsx +29 -0
  66. package/src/components/BossProfileModal.jsx +162 -0
  67. package/src/components/CachedAvatar.jsx +77 -0
  68. package/src/components/ChatPanel.jsx +219 -0
  69. package/src/components/ChatShared.jsx +255 -0
  70. package/src/components/DepartmentDetail.jsx +842 -0
  71. package/src/components/DepartmentView.jsx +367 -0
  72. package/src/components/FileReference.jsx +260 -0
  73. package/src/components/FilesView.jsx +465 -0
  74. package/src/components/GroupChatView.jsx +799 -0
  75. package/src/components/Mailbox.jsx +926 -0
  76. package/src/components/MessagesView.jsx +112 -0
  77. package/src/components/OnboardingGuide.jsx +209 -0
  78. package/src/components/OrgTree.jsx +151 -0
  79. package/src/components/Overview.jsx +391 -0
  80. package/src/components/PixelOffice.jsx +2281 -0
  81. package/src/components/ProviderGrid.jsx +551 -0
  82. package/src/components/ProvidersBoard.jsx +16 -0
  83. package/src/components/RequirementDetail.jsx +1279 -0
  84. package/src/components/RequirementsBoard.jsx +187 -0
  85. package/src/components/SecretarySettings.jsx +295 -0
  86. package/src/components/SetupWizard.jsx +388 -0
  87. package/src/components/Sidebar.jsx +169 -0
  88. package/src/components/SystemMonitor.jsx +808 -0
  89. package/src/components/TalentMarket.jsx +183 -0
  90. package/src/components/TeamDetail.jsx +697 -0
  91. package/src/core/agent/base-agent.js +104 -0
  92. package/src/core/agent/chat-store.js +602 -0
  93. package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
  94. package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
  95. package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
  96. package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
  97. package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
  98. package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
  99. package/src/core/agent/cli-agent/backends/index.js +27 -0
  100. package/src/core/agent/cli-agent/backends/registry.js +580 -0
  101. package/src/core/agent/cli-agent/index.js +154 -0
  102. package/src/core/agent/index.js +60 -0
  103. package/src/core/agent/llm-agent/client.js +320 -0
  104. package/src/core/agent/llm-agent/index.js +97 -0
  105. package/src/core/agent/message-bus.js +211 -0
  106. package/src/core/agent/session.js +608 -0
  107. package/src/core/agent/tools.js +596 -0
  108. package/src/core/agent/web-agent/backends/base-backend.js +180 -0
  109. package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
  110. package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
  111. package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
  112. package/src/core/agent/web-agent/backends/index.js +91 -0
  113. package/src/core/agent/web-agent/index.js +278 -0
  114. package/src/core/agent/web-agent/web-client.js +407 -0
  115. package/src/core/employee/base-employee.js +1088 -0
  116. package/src/core/employee/index.js +35 -0
  117. package/src/core/employee/knowledge.js +327 -0
  118. package/src/core/employee/lifecycle.js +990 -0
  119. package/src/core/employee/memory/index.js +642 -0
  120. package/src/core/employee/memory/store.js +143 -0
  121. package/src/core/employee/performance.js +224 -0
  122. package/src/core/employee/secretary.js +625 -0
  123. package/src/core/employee/skills.js +398 -0
  124. package/src/core/index.js +38 -0
  125. package/src/core/organization/company.js +2600 -0
  126. package/src/core/organization/department.js +737 -0
  127. package/src/core/organization/group-chat-loop.js +264 -0
  128. package/src/core/organization/index.js +8 -0
  129. package/src/core/organization/persistence.js +111 -0
  130. package/src/core/organization/team.js +267 -0
  131. package/src/core/organization/workforce/hr.js +377 -0
  132. package/src/core/organization/workforce/providers.js +468 -0
  133. package/src/core/organization/workforce/role-archetypes.js +805 -0
  134. package/src/core/organization/workforce/talent-market.js +205 -0
  135. package/src/core/prompts.js +532 -0
  136. package/src/core/requirement.js +1789 -0
  137. package/src/core/system/audit.js +483 -0
  138. package/src/core/system/cron.js +449 -0
  139. package/src/core/system/index.js +7 -0
  140. package/src/core/system/plugin.js +2183 -0
  141. package/src/core/utils/json-parse.js +188 -0
  142. package/src/core/workspace.js +239 -0
  143. package/src/lib/api-i18n.js +211 -0
  144. package/src/lib/avatar.js +268 -0
  145. package/src/lib/client-store.js +1025 -0
  146. package/src/lib/config-validator.js +483 -0
  147. package/src/lib/format-time.js +22 -0
  148. package/src/lib/hooks.js +414 -0
  149. package/src/lib/i18n.js +134 -0
  150. package/src/lib/paths.js +23 -0
  151. package/src/lib/store.js +72 -0
  152. package/src/locales/de.js +393 -0
  153. package/src/locales/en.js +1054 -0
  154. package/src/locales/es.js +393 -0
  155. package/src/locales/fr.js +393 -0
  156. package/src/locales/ja.js +501 -0
  157. package/src/locales/ko.js +513 -0
  158. package/src/locales/zh.js +828 -0
  159. package/tailwind.config.mjs +11 -0
@@ -0,0 +1,808 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { useStore } from '@/lib/client-store';
5
+ import { useI18n } from '@/lib/i18n';
6
+ import ProviderGrid from './ProviderGrid';
7
+
8
+ /**
9
+ * SystemMonitor - Operational dashboard for enterprise subsystems
10
+ * User-facing: manage cron jobs, toggle plugins, view system health
11
+ */
12
+ export default function SystemMonitor({ embedded = false }) {
13
+ const { t } = useI18n();
14
+ const { company, fetchCronJobs, createCronJob, manageCronJob, fetchPlugins, managePlugin, fetchSkills, manageSkill, fetchKnowledge, searchKnowledge, manageKnowledge, fetchSystemStatus, factoryReset } = useStore();
15
+ const [activeSection, setActiveSection] = useState('providers');
16
+ const [cronData, setCronData] = useState({ summary: {}, jobs: [] });
17
+ const [plugins, setPlugins] = useState([]);
18
+ const [skills, setSkills] = useState([]);
19
+ const [knowledge, setKnowledge] = useState({ bases: [], stats: {} });
20
+ const [systemStatus, setSystemStatus] = useState(null);
21
+ const [loading, setLoading] = useState(true);
22
+
23
+ // Knowledge base state
24
+ const [showCreateKB, setShowCreateKB] = useState(false);
25
+ const [newKB, setNewKB] = useState({ name: '', description: '', type: 'global' });
26
+ const [showAddEntry, setShowAddEntry] = useState(null); // kbId or null
27
+ const [newEntry, setNewEntry] = useState({ title: '', content: '', entryType: 'note', tags: '' });
28
+ const [kbSearchQuery, setKbSearchQuery] = useState('');
29
+ const [kbSearchResults, setKbSearchResults] = useState(null);
30
+
31
+ // Danger zone state
32
+ const [showFactoryReset, setShowFactoryReset] = useState(false);
33
+ const [factoryResetInput, setFactoryResetInput] = useState('');
34
+ const [factoryResetting, setFactoryResetting] = useState(false);
35
+
36
+ // Cron job creation form
37
+ const [showCreateJob, setShowCreateJob] = useState(false);
38
+ const [newJob, setNewJob] = useState({ name: '', cronExpression: '', agentId: '', taskPrompt: '', description: '' });
39
+
40
+ const refresh = useCallback(async () => {
41
+ setLoading(true);
42
+ const [cron, plug, sk, kb, status] = await Promise.all([
43
+ fetchCronJobs(), fetchPlugins(), fetchSkills(), fetchKnowledge(), fetchSystemStatus(),
44
+ ]);
45
+ setCronData(cron || { summary: {}, jobs: [] });
46
+ setPlugins(plug || []);
47
+ setSkills(sk || []);
48
+ setKnowledge(kb || { bases: [], stats: {} });
49
+ setSystemStatus(status);
50
+ setLoading(false);
51
+ }, [fetchCronJobs, fetchPlugins, fetchSkills, fetchKnowledge, fetchSystemStatus]);
52
+
53
+ useEffect(() => { refresh(); }, [refresh]);
54
+
55
+ // Get all agents for cron job assignment
56
+ const allAgents = [];
57
+ if (company?.departments) {
58
+ for (const dept of company.departments) {
59
+ for (const m of (dept.members || [])) {
60
+ allAgents.push({ id: m.id, name: m.name, role: m.role, department: dept.name });
61
+ }
62
+ }
63
+ }
64
+
65
+ const handleCreateJob = async () => {
66
+ if (!newJob.name || !newJob.cronExpression || !newJob.agentId || !newJob.taskPrompt) return;
67
+ try {
68
+ await createCronJob(newJob);
69
+ setShowCreateJob(false);
70
+ setNewJob({ name: '', cronExpression: '', agentId: '', taskPrompt: '', description: '' });
71
+ await refresh();
72
+ } catch {}
73
+ };
74
+
75
+ const handleJobAction = async (action, jobId) => {
76
+ try {
77
+ await manageCronJob(action, jobId);
78
+ await refresh();
79
+ } catch {}
80
+ };
81
+
82
+ const handlePluginToggle = async (pluginId, currentState) => {
83
+ const action = currentState === 'enabled' ? 'disable' : 'enable';
84
+ try {
85
+ await managePlugin(action, pluginId);
86
+ await refresh();
87
+ } catch {}
88
+ };
89
+
90
+
91
+
92
+ const handleSkillToggle = async (skillId, currentState) => {
93
+ const action = currentState === 'enabled' ? 'disable' : 'enable';
94
+ try {
95
+ await manageSkill(action, skillId);
96
+ await refresh();
97
+ } catch {}
98
+ };
99
+
100
+ const handleCreateKB = async () => {
101
+ if (!newKB.name) return;
102
+ try {
103
+ await manageKnowledge('create', newKB);
104
+ setShowCreateKB(false);
105
+ setNewKB({ name: '', description: '', type: 'global' });
106
+ await refresh();
107
+ } catch {}
108
+ };
109
+
110
+ const handleAddEntry = async () => {
111
+ if (!newEntry.title || !newEntry.content) return;
112
+ try {
113
+ await manageKnowledge('addEntry', {
114
+ kbId: showAddEntry,
115
+ ...newEntry,
116
+ tags: newEntry.tags.split(',').map(t => t.trim()).filter(Boolean),
117
+ });
118
+ setShowAddEntry(null);
119
+ setNewEntry({ title: '', content: '', entryType: 'note', tags: '' });
120
+ await refresh();
121
+ } catch {}
122
+ };
123
+
124
+ const handleKBSearch = async () => {
125
+ if (!kbSearchQuery.trim()) return;
126
+ const results = await searchKnowledge(kbSearchQuery);
127
+ setKbSearchResults(results);
128
+ };
129
+
130
+ const SKILL_CATEGORY_ICONS = {
131
+ coding: '💻', analysis: '📊', creative: '✨', communication: '💬',
132
+ automation: '🤖', research: '🔍', design: '🎨', devops: '🚀',
133
+ };
134
+
135
+ const PLUGIN_CATEGORY_MAP = {
136
+ 'builtin-web-search': 'Web & Search',
137
+ 'builtin-web-fetch': 'Web & Search',
138
+ 'builtin-firecrawl': 'Web & Search',
139
+ 'builtin-browser': 'Browser & UI',
140
+ 'builtin-canvas': 'Browser & UI',
141
+ 'builtin-diffs': 'Browser & UI',
142
+ 'builtin-exec': 'Runtime & Execution',
143
+ 'builtin-apply-patch': 'Runtime & Execution',
144
+ 'builtin-memory': 'Memory & Knowledge',
145
+ 'builtin-image': 'Media & Content',
146
+ 'builtin-pdf': 'Media & Content',
147
+ 'builtin-tts': 'Media & Content',
148
+ 'builtin-data-processing': 'Media & Content',
149
+ 'builtin-message': 'Communication',
150
+ 'builtin-reactions': 'Communication',
151
+ 'builtin-bird': 'Communication',
152
+ 'builtin-sessions': 'Sessions & Multi-Agent',
153
+ 'builtin-subagents': 'Sessions & Multi-Agent',
154
+ 'builtin-cron': 'Automation & Infra',
155
+ 'builtin-gateway': 'Automation & Infra',
156
+ 'builtin-nodes': 'Automation & Infra',
157
+ 'builtin-lobster': 'Workflow & AI',
158
+ 'builtin-llm-task': 'Workflow & AI',
159
+ 'builtin-thinking': 'Workflow & AI',
160
+ 'builtin-code-review': 'Code Quality',
161
+ 'builtin-notifications': 'Code Quality',
162
+ };
163
+
164
+ const PLUGIN_CATEGORY_ICONS = {
165
+ 'Web & Search': '🌐',
166
+ 'Browser & UI': '🖥️',
167
+ 'Runtime & Execution': '⚡',
168
+ 'Memory & Knowledge': '🧠',
169
+ 'Media & Content': '🎨',
170
+ 'Communication': '💬',
171
+ 'Sessions & Multi-Agent': '🤖',
172
+ 'Automation & Infra': '⚙️',
173
+ 'Workflow & AI': '🔗',
174
+ 'Code Quality': '✅',
175
+ 'Other': '🧩',
176
+ };
177
+
178
+ const sections = [
179
+ { id: 'providers', icon: '⚡', label: t('sidebar.nav.providers') },
180
+ { id: 'cron', icon: '⏰', label: t('systemSettings.cards.cron') },
181
+ { id: 'plugins', icon: '🧩', label: t('systemSettings.cards.plugins') },
182
+ { id: 'skills', icon: '📚', label: t('systemSettings.cards.skills') },
183
+ { id: 'knowledge', icon: '🧠', label: t('systemSettings.cards.knowledge') },
184
+ { id: 'health', icon: '💓', label: t('systemSettings.health.title') },
185
+ { id: 'danger', icon: '⚠️', label: t('systemSettings.dangerZone.title') },
186
+ ];
187
+
188
+ return (
189
+ <div className={embedded ? 'space-y-6' : 'p-6 space-y-6 animate-fade-in'}>
190
+ {/* Header - 仅独立模式下显示 */}
191
+ {!embedded && (
192
+ <div className="flex items-center justify-between">
193
+ <div>
194
+ <h1 className="text-2xl font-bold">{t('systemSettings.title')}</h1>
195
+ <p className="text-sm text-[var(--muted)] mt-1">{t('systemSettings.subtitle')}</p>
196
+ </div>
197
+ </div>
198
+ )}
199
+
200
+ {/* Section Tabs */}
201
+ <div className="flex gap-2">
202
+ {sections.map(s => (
203
+ <button
204
+ key={s.id}
205
+ onClick={() => setActiveSection(s.id)}
206
+ className={`px-4 py-2 rounded-lg text-sm flex items-center gap-2 transition-all ${
207
+ activeSection === s.id
208
+ ? 'bg-[var(--accent)] text-white'
209
+ : 'bg-white/5 text-[var(--muted)] hover:bg-white/10'
210
+ }`}
211
+ >
212
+ <span>{s.icon}</span>
213
+ <span>{s.label}</span>
214
+ </button>
215
+ ))}
216
+ </div>
217
+
218
+ {/* === PROVIDERS SECTION === */}
219
+ {activeSection === 'providers' && (
220
+ <div className="space-y-4">
221
+ <ProviderGrid
222
+ showDescription
223
+ showSecretary
224
+ />
225
+ </div>
226
+ )}
227
+
228
+ {/* === CRON SECTION === */}
229
+ {activeSection === 'cron' && (
230
+ <div className="space-y-4">
231
+ {/* Quick Stats */}
232
+ <div className="grid grid-cols-4 gap-4">
233
+ <StatCard label={t('systemSettings.cronStats.running')} value={cronData.summary?.running ? '✅' : '❌'} />
234
+ <StatCard label={t('systemSettings.cronStats.jobs')} value={cronData.jobs?.length || 0} />
235
+ <StatCard label={t('systemSettings.cronDetail.activeJobs')} value={cronData.summary?.activeJobs || 0} color="text-green-400" />
236
+ <StatCard label={t('systemSettings.cronDetail.totalRuns')} value={cronData.summary?.totalRuns || 0} />
237
+ </div>
238
+
239
+ {/* Create Job Button */}
240
+ <div className="flex justify-between items-center">
241
+ <h3 className="text-sm font-semibold">{t('systemSettings.cronDetail.jobList')}</h3>
242
+ <button onClick={() => setShowCreateJob(!showCreateJob)} className="btn-primary text-xs">
243
+ {showCreateJob ? '✕ ' + t('common.cancel') : '+ ' + t('systemSettings.cronDetail.createJob')}
244
+ </button>
245
+ </div>
246
+
247
+ {/* Create Job Form */}
248
+ {showCreateJob && (
249
+ <div className="card space-y-3 animate-fade-in">
250
+ <h4 className="text-sm font-medium">{t('systemSettings.cronDetail.createJob')}</h4>
251
+ <div className="grid grid-cols-2 gap-3">
252
+ <input
253
+ className="input text-sm"
254
+ placeholder={t('systemSettings.cronForm.name')}
255
+ value={newJob.name}
256
+ onChange={e => setNewJob({ ...newJob, name: e.target.value })}
257
+ />
258
+ <input
259
+ className="input text-sm"
260
+ placeholder={t('systemSettings.cronForm.schedule')}
261
+ value={newJob.cronExpression}
262
+ onChange={e => setNewJob({ ...newJob, cronExpression: e.target.value })}
263
+ />
264
+ </div>
265
+ <select
266
+ className="input text-sm w-full"
267
+ value={newJob.agentId}
268
+ onChange={e => setNewJob({ ...newJob, agentId: e.target.value })}
269
+ >
270
+ <option value="">{t('systemSettings.cronForm.selectAgent')}</option>
271
+ {allAgents.map(a => (
272
+ <option key={a.id} value={a.id}>{a.name} ({a.role}) — {a.department}</option>
273
+ ))}
274
+ </select>
275
+ <textarea
276
+ className="input text-sm w-full"
277
+ rows={3}
278
+ placeholder={t('systemSettings.cronForm.taskPrompt')}
279
+ value={newJob.taskPrompt}
280
+ onChange={e => setNewJob({ ...newJob, taskPrompt: e.target.value })}
281
+ />
282
+ <div className="text-xs text-[var(--muted)] space-y-1">
283
+ <div>{t('systemSettings.cronForm.scheduleHint')}</div>
284
+ </div>
285
+ <button onClick={handleCreateJob} className="btn-primary text-sm max-w-xs">
286
+ {t('systemSettings.cronDetail.createJob')}
287
+ </button>
288
+ </div>
289
+ )}
290
+
291
+ {/* Job List */}
292
+ {cronData.jobs?.length > 0 ? (
293
+ <div className="space-y-2">
294
+ {cronData.jobs.map(job => (
295
+ <div key={job.id} className="card flex items-center gap-4">
296
+ <span className={`w-2.5 h-2.5 rounded-full shrink-0 ${
297
+ job.status === 'active' ? 'bg-green-500' :
298
+ job.status === 'running' ? 'bg-blue-500 animate-pulse' :
299
+ job.status === 'paused' ? 'bg-yellow-500' :
300
+ job.status === 'error' ? 'bg-red-500' : 'bg-gray-500'
301
+ }`} />
302
+ <div className="flex-1 min-w-0">
303
+ <div className="text-sm font-medium truncate">{job.name}</div>
304
+ <div className="text-[10px] text-[var(--muted)] flex items-center gap-3 mt-0.5">
305
+ <span>📅 {job.cronExpression}</span>
306
+ <span>🔄 {t('systemSettings.cronDetail.runs')}: {job.runCount}</span>
307
+ {job.nextRun && <span>⏭ {new Date(job.nextRun).toLocaleTimeString()}</span>}
308
+ {job.lastError && <span className="text-red-400">⚠ {job.lastError.slice(0, 30)}</span>}
309
+ </div>
310
+ </div>
311
+ <div className="flex items-center gap-1 shrink-0">
312
+ {job.status === 'active' && (
313
+ <button onClick={() => handleJobAction('pause', job.id)} className="btn-ghost text-[10px]" title={t('systemSettings.cronJobActions.pause')}>⏸</button>
314
+ )}
315
+ {job.status === 'paused' && (
316
+ <button onClick={() => handleJobAction('resume', job.id)} className="btn-ghost text-[10px]" title={t('systemSettings.cronJobActions.resume')}>▶️</button>
317
+ )}
318
+ {job.status === 'error' && (
319
+ <button onClick={() => handleJobAction('resume', job.id)} className="btn-ghost text-[10px]" title={t('systemSettings.cronJobActions.retry')}>🔁</button>
320
+ )}
321
+ <button onClick={() => handleJobAction('trigger', job.id)} className="btn-ghost text-[10px]" title={t('systemSettings.cronJobActions.runNow')}>🚀</button>
322
+ <button onClick={() => handleJobAction('delete', job.id)} className="btn-ghost text-[10px] text-red-400" title={t('systemSettings.cronJobActions.delete')}>🗑</button>
323
+ </div>
324
+ </div>
325
+ ))}
326
+ </div>
327
+ ) : (
328
+ <div className="text-center py-12 text-[var(--muted)]">
329
+ <div className="text-4xl mb-3">⏰</div>
330
+ <p className="text-sm">{t('systemSettings.cronDetail.noJobs')}</p>
331
+ <p className="text-xs mt-1">{t('systemSettings.cronDetail.noJobsHint')}</p>
332
+ </div>
333
+ )}
334
+ </div>
335
+ )}
336
+
337
+ {/* === PLUGINS SECTION === */}
338
+ {activeSection === 'plugins' && (
339
+ <div className="space-y-4">
340
+ <div className="grid grid-cols-3 gap-4">
341
+ <StatCard label={t('systemSettings.pluginStats.registered')} value={plugins.length} />
342
+ <StatCard label={t('systemSettings.pluginStats.enabled')} value={plugins.filter(p => p.state === 'enabled').length} color="text-green-400" />
343
+ <StatCard label={t('systemSettings.pluginDetail.totalTools')} value={plugins.reduce((s, p) => s + (p.toolCount || 0), 0)} />
344
+ </div>
345
+
346
+ {plugins.length > 0 ? (
347
+ <div className="space-y-4">
348
+ {Object.entries(
349
+ plugins.reduce((acc, p) => {
350
+ const cat = PLUGIN_CATEGORY_MAP[p.id] || 'Other';
351
+ (acc[cat] = acc[cat] || []).push(p);
352
+ return acc;
353
+ }, {})
354
+ ).map(([category, catPlugins]) => (
355
+ <div key={category} className="card">
356
+ <div className="flex items-center gap-2 mb-3">
357
+ <span className="text-lg">{PLUGIN_CATEGORY_ICONS[category] || '🧩'}</span>
358
+ <h3 className="text-sm font-semibold">{category}</h3>
359
+ <span className="text-[10px] text-[var(--muted)]">{t('providers.pluginsCount', { n: catPlugins.length })}</span>
360
+ </div>
361
+ <div className="space-y-2">
362
+ {catPlugins.map(plugin => (
363
+ <div key={plugin.id} className="flex items-center gap-4 p-2 rounded-lg hover:bg-white/5 transition-all">
364
+ <span className={`w-2.5 h-2.5 rounded-full shrink-0 ${
365
+ plugin.state === 'enabled' ? 'bg-green-500' : 'bg-gray-500'
366
+ }`} />
367
+ <div className="flex-1 min-w-0">
368
+ <div className="flex items-center gap-2">
369
+ <span className="text-sm font-medium">{plugin.name}</span>
370
+ <span className="text-[10px] text-[var(--muted)]">v{plugin.version}</span>
371
+ </div>
372
+ <div className="text-[10px] text-[var(--muted)] mt-0.5">
373
+ {plugin.description}
374
+ {plugin.toolCount > 0 && <span className="ml-2">{t('providers.toolsCount', { n: plugin.toolCount })}</span>}
375
+ {plugin.hookCount > 0 && <span className="ml-2">{t('providers.hooksCount', { n: plugin.hookCount })}</span>}
376
+ </div>
377
+ </div>
378
+ <button
379
+ onClick={() => handlePluginToggle(plugin.id, plugin.state)}
380
+ className={`text-xs px-3 py-1 rounded-full transition-all ${
381
+ plugin.state === 'enabled'
382
+ ? 'bg-green-900/30 text-green-400 hover:bg-red-900/30 hover:text-red-400'
383
+ : 'bg-white/10 text-[var(--muted)] hover:bg-green-900/30 hover:text-green-400'
384
+ }`}
385
+ >
386
+ {plugin.state === 'enabled' ? t('common.disable') : t('common.enable')}
387
+ </button>
388
+ </div>
389
+ ))}
390
+ </div>
391
+ </div>
392
+ ))}
393
+ </div>
394
+ ) : (
395
+ <div className="text-center py-12 text-[var(--muted)]">
396
+ <div className="text-4xl mb-3">🧩</div>
397
+ <p className="text-sm">{t('systemSettings.pluginDetail.noPlugins')}</p>
398
+ </div>
399
+ )}
400
+ </div>
401
+ )}
402
+
403
+ {/* === SKILLS SECTION === */}
404
+ {activeSection === 'skills' && (
405
+ <div className="space-y-4">
406
+ <div className="grid grid-cols-4 gap-4">
407
+ <StatCard label={t('systemSettings.skillStats.total')} value={skills.length} />
408
+ <StatCard label={t('systemSettings.skillStats.enabled')} value={skills.filter(s => s.state === 'enabled').length} color="text-green-400" />
409
+ <StatCard label={t('systemSettings.skillStats.categories')} value={[...new Set(skills.map(s => s.category))].length} />
410
+ <StatCard label={t('systemSettings.skillStats.installed')} value={skills.filter(s => s.state !== 'available').length} />
411
+ </div>
412
+
413
+ {/* Skills grouped by category */}
414
+ {Object.entries(
415
+ skills.reduce((acc, s) => {
416
+ (acc[s.category] = acc[s.category] || []).push(s);
417
+ return acc;
418
+ }, {})
419
+ ).map(([category, catSkills]) => (
420
+ <div key={category} className="card">
421
+ <div className="flex items-center gap-2 mb-3">
422
+ <span className="text-lg">{SKILL_CATEGORY_ICONS[category] || '⚡'}</span>
423
+ <h3 className="text-sm font-semibold capitalize">{category}</h3>
424
+ <span className="text-[10px] text-[var(--muted)]">{t('providers.skillsCount', { n: catSkills.length })}</span>
425
+ </div>
426
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-2">
427
+ {catSkills.map(skill => (
428
+ <div key={skill.id} className={`p-3 rounded-lg border transition-all ${
429
+ skill.state === 'enabled'
430
+ ? 'border-green-500/30 bg-green-900/10'
431
+ : 'border-[var(--border)] bg-[var(--background)]'
432
+ }`}>
433
+ <div className="flex items-center justify-between mb-1">
434
+ <div className="flex items-center gap-2 min-w-0">
435
+ <span>{skill.icon}</span>
436
+ <span className="text-sm font-medium truncate">{skill.name}</span>
437
+ </div>
438
+ <button
439
+ onClick={() => handleSkillToggle(skill.id, skill.state)}
440
+ className={`text-[10px] px-2 py-0.5 rounded-full transition-all ${
441
+ skill.state === 'enabled'
442
+ ? 'bg-green-900/30 text-green-400 hover:bg-red-900/30 hover:text-red-400'
443
+ : 'bg-white/10 text-[var(--muted)] hover:bg-green-900/30 hover:text-green-400'
444
+ }`}
445
+ >
446
+ {skill.state === 'enabled' ? t('common.disable') : t('common.enable')}
447
+ </button>
448
+ </div>
449
+ <p className="text-[10px] text-[var(--muted)] line-clamp-2">{skill.description}</p>
450
+ {skill.tags?.length > 0 && (
451
+ <div className="flex gap-1 flex-wrap mt-1.5">
452
+ {skill.tags.slice(0, 4).map((tag, i) => (
453
+ <span key={i} className="text-[9px] bg-white/5 text-[var(--muted)] px-1.5 py-0.5 rounded">{tag}</span>
454
+ ))}
455
+ </div>
456
+ )}
457
+ </div>
458
+ ))}
459
+ </div>
460
+ </div>
461
+ ))}
462
+
463
+ {skills.length === 0 && (
464
+ <div className="text-center py-12 text-[var(--muted)]">
465
+ <div className="text-4xl mb-3">📚</div>
466
+ <p className="text-sm">{t('systemSettings.skillDetail.noSkills')}</p>
467
+ </div>
468
+ )}
469
+ </div>
470
+ )}
471
+
472
+ {/* === KNOWLEDGE BASE SECTION === */}
473
+ {activeSection === 'knowledge' && (
474
+ <div className="space-y-4">
475
+ <div className="grid grid-cols-4 gap-4">
476
+ <StatCard label={t('systemSettings.kbStats.totalBases')} value={knowledge.stats?.totalBases || 0} />
477
+ <StatCard label={t('systemSettings.kbStats.enabled')} value={knowledge.stats?.enabledBases || 0} color="text-green-400" />
478
+ <StatCard label={t('systemSettings.kbStats.totalEntries')} value={knowledge.stats?.totalEntries || 0} />
479
+ <StatCard label={t('systemSettings.kbStats.globalBases')} value={knowledge.stats?.byType?.global || 0} />
480
+ </div>
481
+
482
+ {/* Search */}
483
+ <div className="card">
484
+ <div className="flex gap-2">
485
+ <input
486
+ className="input flex-1 text-sm"
487
+ placeholder={t('systemSettings.kbSearch.placeholder')}
488
+ value={kbSearchQuery}
489
+ onChange={e => setKbSearchQuery(e.target.value)}
490
+ onKeyDown={e => e.key === 'Enter' && handleKBSearch()}
491
+ />
492
+ <button onClick={handleKBSearch} className="btn-primary text-sm">
493
+ 🔍 {t('systemSettings.kbSearch.btn')}
494
+ </button>
495
+ </div>
496
+ {kbSearchResults && (
497
+ <div className="mt-3 space-y-2">
498
+ {kbSearchResults.length > 0 ? kbSearchResults.map((r, i) => (
499
+ <div key={i} className="p-2 rounded bg-white/5 text-sm">
500
+ <div className="flex items-center gap-2">
501
+ <span className="text-[10px] bg-blue-900/30 text-blue-400 px-1.5 py-0.5 rounded">{r.type}</span>
502
+ <span className="font-medium">{r.title}</span>
503
+ <span className="text-[10px] text-[var(--muted)] ml-auto">{r.knowledgeBaseName}</span>
504
+ </div>
505
+ <p className="text-[10px] text-[var(--muted)] mt-1 line-clamp-2">{r.content}</p>
506
+ </div>
507
+ )) : (
508
+ <p className="text-xs text-[var(--muted)]">{t('systemSettings.kbSearch.noResults')}</p>
509
+ )}
510
+ </div>
511
+ )}
512
+ </div>
513
+
514
+ {/* Actions */}
515
+ <div className="flex justify-between items-center">
516
+ <h3 className="text-sm font-semibold">{t('systemSettings.kbDetail.listTitle')}</h3>
517
+ <button onClick={() => setShowCreateKB(!showCreateKB)} className="btn-primary text-xs">
518
+ {showCreateKB ? '✕ ' + t('common.cancel') : '+ ' + t('systemSettings.kbDetail.createKB')}
519
+ </button>
520
+ </div>
521
+
522
+ {/* Create KB Form */}
523
+ {showCreateKB && (
524
+ <div className="card space-y-3 animate-fade-in">
525
+ <h4 className="text-sm font-medium">{t('systemSettings.kbDetail.createKB')}</h4>
526
+ <input
527
+ className="input text-sm w-full"
528
+ placeholder={t('systemSettings.kbForm.name')}
529
+ value={newKB.name}
530
+ onChange={e => setNewKB({ ...newKB, name: e.target.value })}
531
+ />
532
+ <input
533
+ className="input text-sm w-full"
534
+ placeholder={t('systemSettings.kbForm.description')}
535
+ value={newKB.description}
536
+ onChange={e => setNewKB({ ...newKB, description: e.target.value })}
537
+ />
538
+ <select
539
+ className="input text-sm w-full"
540
+ value={newKB.type}
541
+ onChange={e => setNewKB({ ...newKB, type: e.target.value })}
542
+ >
543
+ <option value="global">{t('systemSettings.kbForm.typeGlobal')}</option>
544
+ <option value="department">{t('systemSettings.kbForm.typeDept')}</option>
545
+ <option value="agent">{t('systemSettings.kbForm.typeAgent')}</option>
546
+ </select>
547
+ <button onClick={handleCreateKB} className="btn-primary text-sm max-w-xs">
548
+ {t('systemSettings.kbDetail.createKB')}
549
+ </button>
550
+ </div>
551
+ )}
552
+
553
+ {/* KB List */}
554
+ {(knowledge.bases || []).length > 0 ? (
555
+ <div className="space-y-2">
556
+ {knowledge.bases.map(kb => (
557
+ <div key={kb.id} className="card">
558
+ <div className="flex items-center gap-3">
559
+ <span className={`w-2.5 h-2.5 rounded-full shrink-0 ${kb.enabled ? 'bg-green-500' : 'bg-gray-500'}`} />
560
+ <div className="flex-1 min-w-0">
561
+ <div className="flex items-center gap-2">
562
+ <span className="text-sm font-medium">{kb.name}</span>
563
+ <span className="text-[10px] bg-blue-900/20 text-blue-400 px-1.5 py-0.5 rounded">{kb.type}</span>
564
+ <span className="text-[10px] text-[var(--muted)]">{kb.entryCount} {t('systemSettings.kbStats.totalEntries').toLowerCase()}</span>
565
+ </div>
566
+ {kb.description && <p className="text-[10px] text-[var(--muted)] mt-0.5">{kb.description}</p>}
567
+ </div>
568
+ <div className="flex items-center gap-1 shrink-0">
569
+ <button
570
+ onClick={() => { setShowAddEntry(showAddEntry === kb.id ? null : kb.id); setNewEntry({ title: '', content: '', entryType: 'note', tags: '' }); }}
571
+ className="btn-ghost text-[10px]"
572
+ title={t('systemSettings.kbDetail.addEntry')}
573
+ >➕</button>
574
+ <button
575
+ onClick={async () => { await manageKnowledge('toggle', { kbId: kb.id }); await refresh(); }}
576
+ className={`text-[10px] px-2 py-0.5 rounded-full ${kb.enabled ? 'bg-green-900/30 text-green-400' : 'bg-white/10 text-[var(--muted)]'}`}
577
+ >
578
+ {kb.enabled ? t('common.enable') : t('common.disable')}
579
+ </button>
580
+ </div>
581
+ </div>
582
+
583
+ {/* Add Entry Form */}
584
+ {showAddEntry === kb.id && (
585
+ <div className="mt-3 p-3 rounded-lg bg-white/5 space-y-2 animate-fade-in">
586
+ <input
587
+ className="input text-sm w-full"
588
+ placeholder={t('systemSettings.kbForm.entryTitle')}
589
+ value={newEntry.title}
590
+ onChange={e => setNewEntry({ ...newEntry, title: e.target.value })}
591
+ />
592
+ <textarea
593
+ className="input text-sm w-full"
594
+ rows={3}
595
+ placeholder={t('systemSettings.kbForm.entryContent')}
596
+ value={newEntry.content}
597
+ onChange={e => setNewEntry({ ...newEntry, content: e.target.value })}
598
+ />
599
+ <div className="flex gap-2">
600
+ <select
601
+ className="input text-sm flex-1"
602
+ value={newEntry.entryType}
603
+ onChange={e => setNewEntry({ ...newEntry, entryType: e.target.value })}
604
+ >
605
+ <option value="note">{t('systemSettings.kbEntryTypes.note')}</option>
606
+ <option value="fact">{t('systemSettings.kbEntryTypes.fact')}</option>
607
+ <option value="decision">{t('systemSettings.kbEntryTypes.decision')}</option>
608
+ <option value="procedure">{t('systemSettings.kbEntryTypes.procedure')}</option>
609
+ <option value="reference">{t('systemSettings.kbEntryTypes.reference')}</option>
610
+ <option value="faq">{t('systemSettings.kbEntryTypes.faq')}</option>
611
+ </select>
612
+ <input
613
+ className="input text-sm flex-1"
614
+ placeholder={t('systemSettings.kbForm.tags')}
615
+ value={newEntry.tags}
616
+ onChange={e => setNewEntry({ ...newEntry, tags: e.target.value })}
617
+ />
618
+ </div>
619
+ <button onClick={handleAddEntry} className="btn-primary text-sm max-w-xs">
620
+ {t('systemSettings.kbDetail.addEntry')}
621
+ </button>
622
+ </div>
623
+ )}
624
+ </div>
625
+ ))}
626
+ </div>
627
+ ) : (
628
+ <div className="text-center py-12 text-[var(--muted)]">
629
+ <div className="text-4xl mb-3">🧠</div>
630
+ <p className="text-sm">{t('systemSettings.kbDetail.noKB')}</p>
631
+ <p className="text-xs mt-1">{t('systemSettings.kbDetail.noKBHint')}</p>
632
+ </div>
633
+ )}
634
+ </div>
635
+ )}
636
+
637
+ {/* === HEALTH SECTION === */}
638
+ {activeSection === 'health' && systemStatus && (
639
+ <div className="space-y-4">
640
+ {/* Overview Grid */}
641
+ <div className="grid grid-cols-4 gap-4">
642
+ <StatCard icon="🛡️" label={t('systemSettings.cards.audit')} value={systemStatus.audit?.total || 0} sub={`${systemStatus.audit?.blocked || 0} ${t('systemSettings.auditStats.blocked')}`} />
643
+ <StatCard icon="🔀" label={t('systemSettings.cards.routing')} value={systemStatus.routing?.healthDashboard?.length || 0} sub={systemStatus.routing?.strategy || ''} />
644
+ <StatCard icon="🪝" label={t('systemSettings.cards.hooks')} value={systemStatus.hooks?.totalHandlers || 0} sub={`${systemStatus.hooks?.registeredKeys || 0} ${t('systemSettings.hookStats.eventKeys')}`} />
645
+ <StatCard icon="💬" label={t('systemSettings.cards.sessions')} value={systemStatus.sessions?.totalSessions || 0} sub={`${systemStatus.sessions?.totalMessages || 0} ${t('systemSettings.sessionStats.messages')}`} />
646
+ </div>
647
+
648
+ {/* Provider Health */}
649
+ {systemStatus.routing?.healthDashboard?.length > 0 && (
650
+ <div className="card">
651
+ <h3 className="text-sm font-semibold mb-3">{t('systemSettings.health.providerHealth')}</h3>
652
+ <div className="space-y-2">
653
+ {systemStatus.routing.healthDashboard.map((p, i) => (
654
+ <div key={i} className="flex items-center gap-3 p-2 rounded-lg bg-white/5">
655
+ <span className={`w-2 h-2 rounded-full ${
656
+ p.health === 'healthy' ? 'bg-green-500' : p.health === 'degraded' ? 'bg-yellow-500' : 'bg-red-500'
657
+ }`} />
658
+ <span className="text-sm flex-1">{p.id}</span>
659
+ <span className="text-xs text-[var(--muted)]">
660
+ {p.successRate !== undefined ? `${(p.successRate * 100).toFixed(0)}%` : '-'}
661
+ </span>
662
+ <span className={`text-xs px-2 py-0.5 rounded ${
663
+ p.health === 'healthy' ? 'bg-green-900/30 text-green-400' :
664
+ p.health === 'degraded' ? 'bg-yellow-900/30 text-yellow-400' : 'bg-red-900/30 text-red-400'
665
+ }`}>{p.health}</span>
666
+ </div>
667
+ ))}
668
+ </div>
669
+ </div>
670
+ )}
671
+
672
+ {/* Recent Audit Events */}
673
+ {systemStatus.recentAuditEvents?.length > 0 && (
674
+ <div className="card">
675
+ <h3 className="text-sm font-semibold mb-3">{t('systemSettings.health.recentAudit')}</h3>
676
+ <div className="space-y-1 max-h-60 overflow-auto">
677
+ {systemStatus.recentAuditEvents.slice(0, 15).map((evt, i) => (
678
+ <div key={i} className="flex items-center gap-3 text-xs py-1.5 px-2 rounded bg-white/5">
679
+ <span className={`w-2 h-2 rounded-full shrink-0 ${
680
+ evt.level === 'critical' ? 'bg-red-500' : evt.level === 'warn' ? 'bg-yellow-500' : 'bg-blue-500'
681
+ }`} />
682
+ <span className="text-[var(--muted)] w-20 shrink-0">{evt.category}</span>
683
+ <span className="flex-1 truncate">{evt.action}</span>
684
+ {evt.blocked && <span className="text-red-400 shrink-0">🚫</span>}
685
+ </div>
686
+ ))}
687
+ </div>
688
+ </div>
689
+ )}
690
+ </div>
691
+ )}
692
+
693
+ {/* === DANGER ZONE SECTION === */}
694
+ {activeSection === 'danger' && (
695
+ <div className="space-y-4">
696
+ {/* Warning banner */}
697
+ <div className="card bg-gradient-to-r from-red-900/20 to-orange-900/20 border-red-500/30">
698
+ <div className="flex items-start gap-3">
699
+ <span className="text-3xl">☢️</span>
700
+ <div>
701
+ <h3 className="font-semibold text-red-400">{t('systemSettings.dangerZone.title')}</h3>
702
+ <p className="text-sm text-[var(--muted)] mt-1">{t('systemSettings.dangerZone.subtitle')}</p>
703
+ </div>
704
+ </div>
705
+ </div>
706
+
707
+ {/* Factory Reset card */}
708
+ <div className="card border-red-500/20">
709
+ <div className="flex items-center justify-between">
710
+ <div className="flex-1">
711
+ <h4 className="font-semibold text-red-400">{t('systemSettings.dangerZone.factoryReset')}</h4>
712
+ <p className="text-sm text-[var(--muted)] mt-1 max-w-xl">
713
+ {t('systemSettings.dangerZone.factoryResetDesc')}
714
+ </p>
715
+ </div>
716
+ <button
717
+ onClick={() => { setShowFactoryReset(true); setFactoryResetInput(''); }}
718
+ className="shrink-0 ml-4 px-4 py-2.5 rounded-lg bg-red-600/20 hover:bg-red-600/30 text-red-400 border border-red-500/30 hover:border-red-500/50 transition-all text-sm font-medium cursor-pointer"
719
+ >
720
+ {t('systemSettings.dangerZone.factoryResetBtn')}
721
+ </button>
722
+ </div>
723
+ </div>
724
+
725
+ {/* Factory Reset confirmation modal */}
726
+ {showFactoryReset && (
727
+ <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 !m-0" onClick={() => !factoryResetting && setShowFactoryReset(false)}>
728
+ <div className="card max-w-md w-full mx-4 space-y-4 border-red-500/30" onClick={e => e.stopPropagation()}>
729
+ <div className="flex items-center gap-3">
730
+ <span className="text-3xl">💀</span>
731
+ <h3 className="text-lg font-bold text-red-400">{t('systemSettings.dangerZone.factoryResetConfirm')}</h3>
732
+ </div>
733
+
734
+ <p className="text-sm text-[var(--muted)]">
735
+ {t('systemSettings.dangerZone.factoryResetConfirmDesc')}
736
+ </p>
737
+
738
+ <ul className="space-y-1.5">
739
+ {t('systemSettings.dangerZone.factoryResetItems').map((item, i) => (
740
+ <li key={i} className="text-sm text-red-300/80 flex items-center gap-2">
741
+ <span className="w-1.5 h-1.5 rounded-full bg-red-500 shrink-0" />
742
+ {item}
743
+ </li>
744
+ ))}
745
+ </ul>
746
+
747
+ <div>
748
+ <label className="block text-sm mb-1.5 text-[var(--muted)]">
749
+ {t('systemSettings.dangerZone.factoryResetInput', { companyName: company?.name || 'Company' })}
750
+ </label>
751
+ <input
752
+ className="input w-full"
753
+ placeholder={t('systemSettings.dangerZone.factoryResetInputPlaceholder')}
754
+ value={factoryResetInput}
755
+ onChange={e => setFactoryResetInput(e.target.value)}
756
+ disabled={factoryResetting}
757
+ />
758
+ </div>
759
+
760
+ <div className="flex gap-2">
761
+ <button
762
+ className="btn-secondary flex-1"
763
+ onClick={() => setShowFactoryReset(false)}
764
+ disabled={factoryResetting}
765
+ >
766
+ {t('common.cancel')}
767
+ </button>
768
+ <button
769
+ className="flex-1 px-4 py-2 rounded-lg bg-red-600 hover:bg-red-700 text-white font-medium transition-all disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
770
+ disabled={factoryResetInput !== (company?.name || 'Company') || factoryResetting}
771
+ onClick={async () => {
772
+ setFactoryResetting(true);
773
+ try {
774
+ await factoryReset();
775
+ setShowFactoryReset(false);
776
+ // Reload the page to go back to setup wizard
777
+ setTimeout(() => window.location.reload(), 800);
778
+ } catch (e) {
779
+ setFactoryResetting(false);
780
+ }
781
+ }}
782
+ >
783
+ {factoryResetting ? t('systemSettings.dangerZone.factoryResetting') : t('systemSettings.dangerZone.factoryResetExecute')}
784
+ </button>
785
+ </div>
786
+ </div>
787
+ </div>
788
+ )}
789
+ </div>
790
+ )}
791
+ </div>
792
+ );
793
+ }
794
+
795
+ // ============================================================================
796
+ // Sub-components
797
+ // ============================================================================
798
+
799
+ function StatCard({ icon, label, value, sub, color = '' }) {
800
+ return (
801
+ <div className="card text-center">
802
+ {icon && <div className="text-xl mb-1">{icon}</div>}
803
+ <div className={`text-xl font-bold ${color || 'text-white'}`}>{value}</div>
804
+ <div className="text-[10px] text-[var(--muted)] mt-1">{label}</div>
805
+ {sub && <div className="text-[10px] text-[var(--muted)]">{sub}</div>}
806
+ </div>
807
+ );
808
+ }