@tleblancureta/proto 0.1.2 → 0.3.0

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 (108) hide show
  1. package/core-web/src/ProtoApp.tsx +30 -20
  2. package/core-web/src/components/admin/AdminPanel.tsx +285 -0
  3. package/core-web/src/components/admin/SystemTab.tsx +167 -0
  4. package/core-web/src/components/shell/Toolbar.tsx +11 -1
  5. package/core-web/src/index.ts +1 -0
  6. package/dist/core-gateway/auth.d.ts +7 -0
  7. package/dist/core-gateway/auth.d.ts.map +1 -0
  8. package/dist/core-gateway/auth.js +12 -0
  9. package/dist/core-gateway/auth.js.map +1 -0
  10. package/dist/core-gateway/claude-runner.d.ts +10 -0
  11. package/dist/core-gateway/claude-runner.d.ts.map +1 -0
  12. package/dist/core-gateway/claude-runner.js +206 -0
  13. package/dist/core-gateway/claude-runner.js.map +1 -0
  14. package/dist/core-gateway/config.d.ts +33 -0
  15. package/dist/core-gateway/config.d.ts.map +1 -0
  16. package/dist/core-gateway/config.js +84 -0
  17. package/dist/core-gateway/config.js.map +1 -0
  18. package/dist/core-gateway/email-sender.d.ts +36 -0
  19. package/dist/core-gateway/email-sender.d.ts.map +1 -0
  20. package/dist/core-gateway/email-sender.js +121 -0
  21. package/dist/core-gateway/email-sender.js.map +1 -0
  22. package/dist/core-gateway/mail-ingester.d.ts +3 -0
  23. package/dist/core-gateway/mail-ingester.d.ts.map +1 -0
  24. package/dist/core-gateway/mail-ingester.js +294 -0
  25. package/dist/core-gateway/mail-ingester.js.map +1 -0
  26. package/dist/core-gateway/mail-router.d.ts +18 -0
  27. package/dist/core-gateway/mail-router.d.ts.map +1 -0
  28. package/dist/core-gateway/mail-router.js +37 -0
  29. package/dist/core-gateway/mail-router.js.map +1 -0
  30. package/dist/core-gateway/mail-threads.d.ts +78 -0
  31. package/dist/core-gateway/mail-threads.d.ts.map +1 -0
  32. package/dist/core-gateway/mail-threads.js +100 -0
  33. package/dist/core-gateway/mail-threads.js.map +1 -0
  34. package/dist/core-gateway/rate-limiter.d.ts +9 -0
  35. package/dist/core-gateway/rate-limiter.d.ts.map +1 -0
  36. package/dist/core-gateway/rate-limiter.js +13 -0
  37. package/dist/core-gateway/rate-limiter.js.map +1 -0
  38. package/dist/core-gateway/registry.d.ts +14 -0
  39. package/dist/core-gateway/registry.d.ts.map +1 -0
  40. package/dist/core-gateway/registry.js +83 -0
  41. package/dist/core-gateway/registry.js.map +1 -0
  42. package/dist/core-gateway/routes/admin.d.ts +3 -0
  43. package/dist/core-gateway/routes/admin.d.ts.map +1 -0
  44. package/dist/core-gateway/routes/admin.js +91 -0
  45. package/dist/core-gateway/routes/admin.js.map +1 -0
  46. package/dist/core-gateway/routes/chat.d.ts +9 -0
  47. package/dist/core-gateway/routes/chat.d.ts.map +1 -0
  48. package/dist/core-gateway/routes/chat.js +149 -0
  49. package/dist/core-gateway/routes/chat.js.map +1 -0
  50. package/dist/core-gateway/routes/cron.d.ts +13 -0
  51. package/dist/core-gateway/routes/cron.d.ts.map +1 -0
  52. package/dist/core-gateway/routes/cron.js +79 -0
  53. package/dist/core-gateway/routes/cron.js.map +1 -0
  54. package/dist/core-gateway/routes/gmail.d.ts +8 -0
  55. package/dist/core-gateway/routes/gmail.d.ts.map +1 -0
  56. package/dist/core-gateway/routes/gmail.js +57 -0
  57. package/dist/core-gateway/routes/gmail.js.map +1 -0
  58. package/dist/core-gateway/routes/health.d.ts +3 -0
  59. package/dist/core-gateway/routes/health.d.ts.map +1 -0
  60. package/dist/core-gateway/routes/health.js +7 -0
  61. package/dist/core-gateway/routes/health.js.map +1 -0
  62. package/dist/core-gateway/routes/upload.d.ts +7 -0
  63. package/dist/core-gateway/routes/upload.d.ts.map +1 -0
  64. package/dist/core-gateway/routes/upload.js +31 -0
  65. package/dist/core-gateway/routes/upload.js.map +1 -0
  66. package/dist/core-gateway/scheduler.d.ts +68 -0
  67. package/dist/core-gateway/scheduler.d.ts.map +1 -0
  68. package/dist/core-gateway/scheduler.js +252 -0
  69. package/dist/core-gateway/scheduler.js.map +1 -0
  70. package/dist/core-gateway/server.d.ts +17 -0
  71. package/dist/core-gateway/server.d.ts.map +1 -0
  72. package/dist/core-gateway/server.js +45 -0
  73. package/dist/core-gateway/server.js.map +1 -0
  74. package/dist/core-gateway/session.d.ts +18 -0
  75. package/dist/core-gateway/session.d.ts.map +1 -0
  76. package/dist/core-gateway/session.js +178 -0
  77. package/dist/core-gateway/session.js.map +1 -0
  78. package/dist/core-gateway/skills.d.ts +4 -0
  79. package/dist/core-gateway/skills.d.ts.map +1 -0
  80. package/dist/core-gateway/skills.js +100 -0
  81. package/dist/core-gateway/skills.js.map +1 -0
  82. package/dist/core-gateway/supabase.d.ts +3 -0
  83. package/dist/core-gateway/supabase.d.ts.map +1 -0
  84. package/dist/core-gateway/supabase.js +18 -0
  85. package/dist/core-gateway/supabase.js.map +1 -0
  86. package/dist/core-web/src/ProtoApp.d.ts.map +1 -1
  87. package/dist/core-web/src/ProtoApp.js +5 -3
  88. package/dist/core-web/src/ProtoApp.js.map +1 -1
  89. package/dist/core-web/src/components/admin/AdminPanel.d.ts +7 -0
  90. package/dist/core-web/src/components/admin/AdminPanel.d.ts.map +1 -0
  91. package/dist/core-web/src/components/admin/AdminPanel.js +112 -0
  92. package/dist/core-web/src/components/admin/AdminPanel.js.map +1 -0
  93. package/dist/core-web/src/components/admin/SystemTab.d.ts +2 -0
  94. package/dist/core-web/src/components/admin/SystemTab.d.ts.map +1 -0
  95. package/dist/core-web/src/components/admin/SystemTab.js +25 -0
  96. package/dist/core-web/src/components/admin/SystemTab.js.map +1 -0
  97. package/dist/core-web/src/components/shell/Toolbar.d.ts.map +1 -1
  98. package/dist/core-web/src/components/shell/Toolbar.js +4 -2
  99. package/dist/core-web/src/components/shell/Toolbar.js.map +1 -1
  100. package/dist/core-web/src/index.d.ts +1 -0
  101. package/dist/core-web/src/index.d.ts.map +1 -1
  102. package/dist/core-web/src/index.js +1 -0
  103. package/dist/core-web/src/index.js.map +1 -1
  104. package/dist/gateway.d.ts +3 -0
  105. package/dist/gateway.d.ts.map +1 -0
  106. package/dist/gateway.js +3 -0
  107. package/dist/gateway.js.map +1 -0
  108. package/package.json +22 -1
@@ -18,7 +18,9 @@
18
18
  * }
19
19
  */
20
20
  import { useState, useCallback, useRef, useMemo } from 'react'
21
+ import { BrowserRouter, Routes, Route } from 'react-router-dom'
21
22
  import Shell, { type CockpitDefinition } from './components/Shell.js'
23
+ import { AdminPanel } from './components/admin/AdminPanel.js'
22
24
  import { useAuth } from './hooks/useAuth.js'
23
25
  import { useTheme } from './hooks/useTheme.js'
24
26
  import { buildWidgetRegistry, type WidgetDefinition } from './lib/define-widget.js'
@@ -64,7 +66,7 @@ export function ProtoApp({
64
66
  }: ProtoAppProps) {
65
67
  useTheme()
66
68
 
67
- const { user, companyId, companies, profile, loading, signOut, setCompanyId } = useAuth()
69
+ const { user, role, companyId, companies, profile, loading, signOut, setCompanyId } = useAuth()
68
70
  const [refreshKey, setRefreshKey] = useState(0)
69
71
  const chatSendRef = useRef<((msg: string) => void) | null>(null)
70
72
 
@@ -140,24 +142,32 @@ export function ProtoApp({
140
142
  const effectiveCompanyId = companyId || user.id
141
143
 
142
144
  return (
143
- <Shell
144
- widgets={widgetRegistry}
145
- defaultWidgets={defaultWidgets}
146
- defaultLayouts={defaultLayouts}
147
- cockpits={cockpits}
148
- companyId={effectiveCompanyId}
149
- refreshKey={refreshKey}
150
- onSendToChat={onSendToChat}
151
- activeEntity={activeEntity}
152
- onActivateEntity={activateEntity}
153
- onDeactivateEntity={() => setActiveEntity(null)}
154
- openEntities={openEntities}
155
- onCloseTab={closeEntityTab}
156
- companies={companies}
157
- effectiveCompanyId={effectiveCompanyId}
158
- setCompanyId={setCompanyId}
159
- onSignOut={signOut}
160
- userEmail={profile?.full_name || user.email || ''}
161
- />
145
+ <BrowserRouter>
146
+ <Routes>
147
+ <Route path="/admin" element={<AdminPanel widgets={widgetRegistry} />} />
148
+ <Route path="*" element={
149
+ <Shell
150
+ widgets={widgetRegistry}
151
+ defaultWidgets={defaultWidgets}
152
+ defaultLayouts={defaultLayouts}
153
+ cockpits={cockpits}
154
+ companyId={effectiveCompanyId}
155
+ refreshKey={refreshKey}
156
+ onSendToChat={onSendToChat}
157
+ activeEntity={activeEntity}
158
+ onActivateEntity={activateEntity}
159
+ onDeactivateEntity={() => setActiveEntity(null)}
160
+ openEntities={openEntities}
161
+ onCloseTab={closeEntityTab}
162
+ role={role}
163
+ companies={companies}
164
+ effectiveCompanyId={effectiveCompanyId}
165
+ setCompanyId={setCompanyId}
166
+ onSignOut={signOut}
167
+ userEmail={profile?.full_name || user.email || ''}
168
+ />
169
+ } />
170
+ </Routes>
171
+ </BrowserRouter>
162
172
  )
163
173
  }
@@ -0,0 +1,285 @@
1
+ import { useState, useMemo } from 'react'
2
+ import { useNavigate } from 'react-router-dom'
3
+ import { useAuth } from '../../hooks/useAuth.js'
4
+ import { useData } from '../../hooks/useData.js'
5
+ import { useTheme } from '../../hooks/useTheme.js'
6
+ import { supabase } from '../../lib/supabase.js'
7
+ import { Button } from '../ui/button.js'
8
+ import {
9
+ ArrowLeftIcon, UsersIcon, Building2Icon, WrenchIcon,
10
+ LayoutGridIcon, SearchIcon,
11
+ } from 'lucide-react'
12
+ import { SystemTab } from './SystemTab.js'
13
+ import type { WidgetRegistry } from '../../lib/define-widget.js'
14
+
15
+ type Tab = 'users' | 'companies' | 'widgets' | 'system'
16
+
17
+ interface AdminPanelProps {
18
+ widgets?: WidgetRegistry
19
+ }
20
+
21
+ export function AdminPanel({ widgets }: AdminPanelProps) {
22
+ useTheme()
23
+ const navigate = useNavigate()
24
+ const { role, loading } = useAuth()
25
+ const [activeTab, setActiveTab] = useState<Tab>('users')
26
+
27
+ if (loading) {
28
+ return <div className="flex h-screen items-center justify-center text-muted-foreground">Loading...</div>
29
+ }
30
+
31
+ if (role !== 'admin') {
32
+ return (
33
+ <div className="flex h-screen items-center justify-center text-muted-foreground">
34
+ No tienes acceso a esta seccion.
35
+ </div>
36
+ )
37
+ }
38
+
39
+ const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [
40
+ { id: 'users', label: 'Usuarios', icon: <UsersIcon className="w-4 h-4" /> },
41
+ { id: 'companies', label: 'Empresas', icon: <Building2Icon className="w-4 h-4" /> },
42
+ { id: 'widgets', label: 'Widgets', icon: <LayoutGridIcon className="w-4 h-4" /> },
43
+ { id: 'system', label: 'Sistema', icon: <WrenchIcon className="w-4 h-4" /> },
44
+ ]
45
+
46
+ return (
47
+ <div className="h-screen flex flex-col bg-background text-foreground">
48
+ <div className="border-b border-border px-4 py-3 flex items-center gap-3 bg-background/80 backdrop-blur">
49
+ <Button variant="ghost" size="sm" className="h-8 gap-1.5" onClick={() => navigate('/')}>
50
+ <ArrowLeftIcon className="w-4 h-4" /> Shell
51
+ </Button>
52
+ <div className="h-4 w-px bg-border" />
53
+ <h1 className="text-sm font-semibold">Admin</h1>
54
+ </div>
55
+
56
+ <div className="flex flex-1 overflow-hidden">
57
+ <nav className="w-48 border-r border-border p-2 flex flex-col gap-0.5 shrink-0">
58
+ {tabs.map(tab => (
59
+ <button
60
+ key={tab.id}
61
+ onClick={() => setActiveTab(tab.id)}
62
+ className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors ${
63
+ activeTab === tab.id
64
+ ? 'bg-accent text-foreground font-medium'
65
+ : 'text-muted-foreground hover:text-foreground hover:bg-accent/50'
66
+ }`}
67
+ >
68
+ {tab.icon}
69
+ {tab.label}
70
+ </button>
71
+ ))}
72
+ </nav>
73
+
74
+ <main className="flex-1 overflow-y-auto p-6">
75
+ {activeTab === 'users' && <UsersTab />}
76
+ {activeTab === 'companies' && <CompaniesTab />}
77
+ {activeTab === 'widgets' && <WidgetsTab widgets={widgets} />}
78
+ {activeTab === 'system' && <SystemTab />}
79
+ </main>
80
+ </div>
81
+ </div>
82
+ )
83
+ }
84
+
85
+ /* ── Users Tab ─────────────────────────────────────────── */
86
+
87
+ function UsersTab() {
88
+ const [search, setSearch] = useState('')
89
+
90
+ const { data: users } = useData(async () => {
91
+ const { data: profiles } = await supabase
92
+ .from('profiles')
93
+ .select('id, full_name, role_title, onboarding_completed')
94
+ .order('full_name')
95
+ if (!profiles) return []
96
+
97
+ const { data: companies } = await supabase
98
+ .from('companies')
99
+ .select('id, name, owner_id')
100
+
101
+ const { data: memberships } = await supabase
102
+ .from('company_users')
103
+ .select('user_id, company_id')
104
+
105
+ return profiles.map((p: any) => {
106
+ const owned = (companies || []).filter((c: any) => c.owner_id === p.id)
107
+ const memberOf = (memberships || [])
108
+ .filter((m: any) => m.user_id === p.id)
109
+ .map((m: any) => (companies || []).find((c: any) => c.id === m.company_id))
110
+ .filter(Boolean)
111
+ return {
112
+ ...p,
113
+ role: owned.length > 0 ? 'admin' : memberOf.length > 0 ? 'client' : 'none',
114
+ companies: owned.length > 0 ? owned : memberOf,
115
+ }
116
+ })
117
+ }, [], [])
118
+
119
+ const filtered = useMemo(() => {
120
+ if (!search) return users
121
+ const q = search.toLowerCase()
122
+ return users.filter((u: any) =>
123
+ u.full_name?.toLowerCase().includes(q) ||
124
+ u.role_title?.toLowerCase().includes(q) ||
125
+ u.role?.includes(q)
126
+ )
127
+ }, [users, search])
128
+
129
+ return (
130
+ <div>
131
+ <div className="flex items-center justify-between mb-4">
132
+ <h2 className="text-lg font-semibold">Usuarios</h2>
133
+ <div className="relative">
134
+ <SearchIcon className="w-4 h-4 absolute left-2.5 top-2.5 text-muted-foreground" />
135
+ <input
136
+ type="text"
137
+ placeholder="Buscar..."
138
+ value={search}
139
+ onChange={e => setSearch(e.target.value)}
140
+ className="pl-8 pr-3 py-2 text-sm bg-background border border-border rounded-md w-60 focus:outline-none focus:ring-1 focus:ring-primary"
141
+ />
142
+ </div>
143
+ </div>
144
+ <div className="border border-border rounded-lg overflow-hidden">
145
+ <table className="w-full text-sm">
146
+ <thead>
147
+ <tr className="bg-muted/50 border-b border-border">
148
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Nombre</th>
149
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Cargo</th>
150
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Rol</th>
151
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Empresa</th>
152
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Onboarding</th>
153
+ </tr>
154
+ </thead>
155
+ <tbody>
156
+ {filtered.map((u: any) => (
157
+ <tr key={u.id} className="border-b border-border/50 hover:bg-accent/30 transition-colors">
158
+ <td className="px-4 py-2.5">{u.full_name || <span className="text-muted-foreground italic">Sin nombre</span>}</td>
159
+ <td className="px-4 py-2.5 text-muted-foreground">{u.role_title || '—'}</td>
160
+ <td className="px-4 py-2.5"><RoleBadge role={u.role} /></td>
161
+ <td className="px-4 py-2.5 text-muted-foreground">
162
+ {u.companies.map((c: any) => c.name).join(', ') || '—'}
163
+ </td>
164
+ <td className="px-4 py-2.5">
165
+ {u.onboarding_completed
166
+ ? <span className="text-green-600 dark:text-green-400">Completo</span>
167
+ : <span className="text-amber-600 dark:text-amber-400">Pendiente</span>}
168
+ </td>
169
+ </tr>
170
+ ))}
171
+ {filtered.length === 0 && (
172
+ <tr><td colSpan={5} className="px-4 py-8 text-center text-muted-foreground">No hay usuarios</td></tr>
173
+ )}
174
+ </tbody>
175
+ </table>
176
+ </div>
177
+ <p className="text-xs text-muted-foreground mt-2">{users.length} usuarios total</p>
178
+ </div>
179
+ )
180
+ }
181
+
182
+ /* ── Companies Tab ─────────────────────────────────────── */
183
+
184
+ function CompaniesTab() {
185
+ const { data: companies } = useData(async () => {
186
+ const { data } = await supabase
187
+ .from('companies')
188
+ .select('id, name, owner_id, created_at')
189
+ .order('name')
190
+ if (!data) return []
191
+
192
+ const { data: profiles } = await supabase.from('profiles').select('id, full_name')
193
+ const { data: memberships } = await supabase.from('company_users').select('company_id, user_id')
194
+
195
+ return data.map((c: any) => ({
196
+ ...c,
197
+ owner_name: (profiles || []).find((p: any) => p.id === c.owner_id)?.full_name || '—',
198
+ member_count: (memberships || []).filter((m: any) => m.company_id === c.id).length,
199
+ }))
200
+ }, [], [])
201
+
202
+ return (
203
+ <div>
204
+ <h2 className="text-lg font-semibold mb-4">Empresas</h2>
205
+ <div className="border border-border rounded-lg overflow-hidden">
206
+ <table className="w-full text-sm">
207
+ <thead>
208
+ <tr className="bg-muted/50 border-b border-border">
209
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Nombre</th>
210
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Owner</th>
211
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Miembros</th>
212
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Creada</th>
213
+ </tr>
214
+ </thead>
215
+ <tbody>
216
+ {companies.map((c: any) => (
217
+ <tr key={c.id} className="border-b border-border/50 hover:bg-accent/30 transition-colors">
218
+ <td className="px-4 py-2.5 font-medium">{c.name}</td>
219
+ <td className="px-4 py-2.5 text-muted-foreground">{c.owner_name}</td>
220
+ <td className="px-4 py-2.5 text-muted-foreground">{c.member_count}</td>
221
+ <td className="px-4 py-2.5 text-muted-foreground">
222
+ {new Date(c.created_at).toLocaleDateString()}
223
+ </td>
224
+ </tr>
225
+ ))}
226
+ {companies.length === 0 && (
227
+ <tr><td colSpan={4} className="px-4 py-8 text-center text-muted-foreground">No hay empresas</td></tr>
228
+ )}
229
+ </tbody>
230
+ </table>
231
+ </div>
232
+ <p className="text-xs text-muted-foreground mt-2">{companies.length} empresas total</p>
233
+ </div>
234
+ )
235
+ }
236
+
237
+ /* ── Widgets Tab ───────────────────────────────────────── */
238
+
239
+ function WidgetsTab({ widgets }: { widgets?: WidgetRegistry }) {
240
+ const entries = useMemo(() => {
241
+ if (!widgets) return []
242
+ return Array.from(widgets.values()).map(w => ({
243
+ type: w.type, title: w.title, icon: w.icon || '▦', category: w.category || 'general',
244
+ }))
245
+ }, [widgets])
246
+
247
+ return (
248
+ <div>
249
+ <h2 className="text-lg font-semibold mb-4">Widgets registrados</h2>
250
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
251
+ {entries.map(w => (
252
+ <div key={w.type} className="border border-border rounded-lg p-4 hover:bg-accent/30 transition-colors">
253
+ <div className="flex items-center gap-2 mb-1">
254
+ <span className="text-lg">{w.icon}</span>
255
+ <span className="font-medium text-sm">{w.title}</span>
256
+ </div>
257
+ <div className="flex items-center gap-2 mt-2">
258
+ <span className="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground">{w.type}</span>
259
+ <span className="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground">{w.category}</span>
260
+ </div>
261
+ </div>
262
+ ))}
263
+ {entries.length === 0 && (
264
+ <p className="text-muted-foreground text-sm col-span-3">No hay widgets registrados</p>
265
+ )}
266
+ </div>
267
+ <p className="text-xs text-muted-foreground mt-3">{entries.length} widgets total</p>
268
+ </div>
269
+ )
270
+ }
271
+
272
+ /* ── Shared ────────────────────────────────────────────── */
273
+
274
+ function RoleBadge({ role }: { role: string }) {
275
+ const styles: Record<string, string> = {
276
+ admin: 'bg-primary/10 text-primary border-primary/20',
277
+ client: 'bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20',
278
+ none: 'bg-muted text-muted-foreground border-border',
279
+ }
280
+ return (
281
+ <span className={`text-xs px-2 py-0.5 rounded-full border ${styles[role] || styles.none}`}>
282
+ {role}
283
+ </span>
284
+ )
285
+ }
@@ -0,0 +1,167 @@
1
+ import { useData } from '../../hooks/useData.js'
2
+ import { GATEWAY_URL, INTERNAL_SECRET } from '../../lib/config.js'
3
+ import {
4
+ WrenchIcon, BoxIcon, GitBranchIcon, BookOpenIcon,
5
+ } from 'lucide-react'
6
+
7
+ interface McpMeta {
8
+ tools: { name: string; description: string }[]
9
+ entities: { name: string; table: string }[]
10
+ workflows: { name: string; entityTable: string; phases: string[] }[]
11
+ skills: { name: string; description: string | null; mcp_tools: string[] }[]
12
+ }
13
+
14
+ export function SystemTab() {
15
+ const { data: meta } = useData<McpMeta | null>(async () => {
16
+ try {
17
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' }
18
+ if (INTERNAL_SECRET) headers['X-Internal-Secret'] = INTERNAL_SECRET
19
+ const res = await fetch(`${GATEWAY_URL}/admin/meta`, { headers })
20
+ if (!res.ok) return null
21
+ return res.json()
22
+ } catch {
23
+ return null
24
+ }
25
+ }, [], null)
26
+
27
+ if (!meta) {
28
+ return (
29
+ <div>
30
+ <h2 className="text-lg font-semibold mb-4">Sistema</h2>
31
+ <p className="text-muted-foreground text-sm">
32
+ No se pudo conectar al gateway. Asegurate de que esta corriendo y que el endpoint <code>/admin/meta</code> esta disponible.
33
+ </p>
34
+ </div>
35
+ )
36
+ }
37
+
38
+ return (
39
+ <div className="space-y-8">
40
+ {/* Tools */}
41
+ <section>
42
+ <div className="flex items-center gap-2 mb-3">
43
+ <WrenchIcon className="w-4 h-4 text-muted-foreground" />
44
+ <h2 className="text-lg font-semibold">Tools</h2>
45
+ <span className="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground">{meta.tools.length}</span>
46
+ </div>
47
+ <div className="border border-border rounded-lg overflow-hidden">
48
+ <table className="w-full text-sm">
49
+ <thead>
50
+ <tr className="bg-muted/50 border-b border-border">
51
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Nombre</th>
52
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Descripcion</th>
53
+ </tr>
54
+ </thead>
55
+ <tbody>
56
+ {meta.tools.map(t => (
57
+ <tr key={t.name} className="border-b border-border/50 hover:bg-accent/30 transition-colors">
58
+ <td className="px-4 py-2 font-mono text-xs">{t.name}</td>
59
+ <td className="px-4 py-2 text-muted-foreground">{t.description}</td>
60
+ </tr>
61
+ ))}
62
+ </tbody>
63
+ </table>
64
+ </div>
65
+ </section>
66
+
67
+ {/* Entities */}
68
+ <section>
69
+ <div className="flex items-center gap-2 mb-3">
70
+ <BoxIcon className="w-4 h-4 text-muted-foreground" />
71
+ <h2 className="text-lg font-semibold">Entities</h2>
72
+ <span className="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground">{meta.entities.length}</span>
73
+ </div>
74
+ {meta.entities.length > 0 ? (
75
+ <div className="border border-border rounded-lg overflow-hidden">
76
+ <table className="w-full text-sm">
77
+ <thead>
78
+ <tr className="bg-muted/50 border-b border-border">
79
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Nombre</th>
80
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Tabla</th>
81
+ </tr>
82
+ </thead>
83
+ <tbody>
84
+ {meta.entities.map((e: any) => (
85
+ <tr key={e.name} className="border-b border-border/50 hover:bg-accent/30 transition-colors">
86
+ <td className="px-4 py-2 font-medium">{e.name}</td>
87
+ <td className="px-4 py-2 font-mono text-xs text-muted-foreground">{e.table}</td>
88
+ </tr>
89
+ ))}
90
+ </tbody>
91
+ </table>
92
+ </div>
93
+ ) : (
94
+ <p className="text-sm text-muted-foreground">No hay entities definidas</p>
95
+ )}
96
+ </section>
97
+
98
+ {/* Workflows */}
99
+ <section>
100
+ <div className="flex items-center gap-2 mb-3">
101
+ <GitBranchIcon className="w-4 h-4 text-muted-foreground" />
102
+ <h2 className="text-lg font-semibold">Workflows</h2>
103
+ <span className="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground">{meta.workflows.length}</span>
104
+ </div>
105
+ {meta.workflows.length > 0 ? (
106
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
107
+ {meta.workflows.map((w: any) => (
108
+ <div key={w.name} className="border border-border rounded-lg p-4">
109
+ <div className="font-medium text-sm mb-1">{w.name}</div>
110
+ <div className="text-xs text-muted-foreground mb-2">Tabla: <code>{w.entityTable}</code></div>
111
+ <div className="flex flex-wrap gap-1">
112
+ {w.phases.map((p: string, i: number) => (
113
+ <span key={p} className="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground flex items-center gap-1">
114
+ {i > 0 && <span className="text-muted-foreground/40">&rarr;</span>}
115
+ {p}
116
+ </span>
117
+ ))}
118
+ </div>
119
+ </div>
120
+ ))}
121
+ </div>
122
+ ) : (
123
+ <p className="text-sm text-muted-foreground">No hay workflows definidos</p>
124
+ )}
125
+ </section>
126
+
127
+ {/* Skills */}
128
+ <section>
129
+ <div className="flex items-center gap-2 mb-3">
130
+ <BookOpenIcon className="w-4 h-4 text-muted-foreground" />
131
+ <h2 className="text-lg font-semibold">Skills</h2>
132
+ <span className="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground">{meta.skills.length}</span>
133
+ </div>
134
+ {meta.skills.length > 0 ? (
135
+ <div className="border border-border rounded-lg overflow-hidden">
136
+ <table className="w-full text-sm">
137
+ <thead>
138
+ <tr className="bg-muted/50 border-b border-border">
139
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Nombre</th>
140
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Descripcion</th>
141
+ <th className="text-left px-4 py-2 font-medium text-muted-foreground">Tools</th>
142
+ </tr>
143
+ </thead>
144
+ <tbody>
145
+ {meta.skills.map((s: any) => (
146
+ <tr key={s.name} className="border-b border-border/50 hover:bg-accent/30 transition-colors">
147
+ <td className="px-4 py-2 font-medium">{s.name}</td>
148
+ <td className="px-4 py-2 text-muted-foreground">{s.description || '—'}</td>
149
+ <td className="px-4 py-2">
150
+ <div className="flex flex-wrap gap-1">
151
+ {s.mcp_tools.map((t: string) => (
152
+ <span key={t} className="text-xs px-1.5 py-0.5 bg-muted rounded text-muted-foreground font-mono">{t}</span>
153
+ ))}
154
+ </div>
155
+ </td>
156
+ </tr>
157
+ ))}
158
+ </tbody>
159
+ </table>
160
+ </div>
161
+ ) : (
162
+ <p className="text-sm text-muted-foreground">No hay skills cargados</p>
163
+ )}
164
+ </section>
165
+ </div>
166
+ )
167
+ }
@@ -1,7 +1,8 @@
1
1
  import { useState, useCallback, type ReactNode } from 'react'
2
+ import { useNavigate } from 'react-router-dom'
2
3
  import { useMountEffect } from '../../hooks/useMountEffect.js'
3
4
  import { Button } from '../ui/button.js'
4
- import { PlusIcon, RotateCcwIcon, SunIcon, MoonIcon, MonitorIcon, UserIcon, LogOutIcon, Building2Icon, ChevronDownIcon, CheckIcon, XIcon, HomeIcon, SettingsIcon, LayoutGridIcon } from 'lucide-react'
5
+ import { PlusIcon, RotateCcwIcon, SunIcon, MoonIcon, MonitorIcon, UserIcon, LogOutIcon, Building2Icon, ChevronDownIcon, CheckIcon, XIcon, HomeIcon, SettingsIcon, LayoutGridIcon, ShieldIcon } from 'lucide-react'
5
6
  import { useTheme, type Theme } from '../../hooks/useTheme.js'
6
7
  import type { ActiveEntity, WidgetType } from './types.js'
7
8
 
@@ -55,6 +56,7 @@ export function Toolbar({
55
56
  document.addEventListener('keydown', handler)
56
57
  return () => document.removeEventListener('keydown', handler)
57
58
  })
59
+ const navigate = useNavigate()
58
60
  const currentCompany = companies?.find(c => c.id === effectiveCompanyId)
59
61
 
60
62
  return (
@@ -197,6 +199,14 @@ export function Toolbar({
197
199
  <SettingsIcon className="w-3.5 h-3.5" /> Configuracion
198
200
  </button>
199
201
  )}
202
+ {role === 'admin' && (
203
+ <button
204
+ onClick={() => { setOpenDropdown(null); navigate('/admin') }}
205
+ className="w-full text-left px-3 py-1.5 text-sm rounded hover:bg-accent transition-colors flex items-center gap-2"
206
+ >
207
+ <ShieldIcon className="w-3.5 h-3.5" /> Admin
208
+ </button>
209
+ )}
200
210
  <ThemeToggleRow />
201
211
  <div className="border-t border-border/50 mt-1 pt-1">
202
212
  <button
@@ -8,6 +8,7 @@
8
8
  // Framework components
9
9
  export { default as Shell, type CockpitDefinition } from './components/Shell.js'
10
10
  export { ProtoApp, type ProtoAppProps } from './ProtoApp.js'
11
+ export { AdminPanel } from './components/admin/AdminPanel.js'
11
12
 
12
13
  // Extension API
13
14
  export {
@@ -0,0 +1,7 @@
1
+ import { Context, Next } from 'hono';
2
+ export declare function verifySecret(c: Context, next: Next): Promise<(Response & import("hono").TypedResponse<{
3
+ error: string;
4
+ }, 500, "json">) | (Response & import("hono").TypedResponse<{
5
+ error: string;
6
+ }, 401, "json">) | undefined>;
7
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../core-gateway/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAGpC,wBAAsB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI;;;;8BASxD"}
@@ -0,0 +1,12 @@
1
+ import { INTERNAL_SECRET } from './config.js';
2
+ export async function verifySecret(c, next) {
3
+ if (!INTERNAL_SECRET) {
4
+ return c.json({ error: 'INTERNAL_API_SECRET not configured' }, 500);
5
+ }
6
+ const secret = c.req.header('x-internal-secret');
7
+ if (secret !== INTERNAL_SECRET) {
8
+ return c.json({ error: 'Invalid secret' }, 401);
9
+ }
10
+ await next();
11
+ }
12
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../core-gateway/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE7C,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,CAAU,EAAE,IAAU;IACvD,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,oCAAoC,EAAE,EAAE,GAAG,CAAC,CAAA;IACrE,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAA;IAChD,IAAI,MAAM,KAAK,eAAe,EAAE,CAAC;QAC/B,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,GAAG,CAAC,CAAA;IACjD,CAAC;IACD,MAAM,IAAI,EAAE,CAAA;AACd,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { ChatRequest, ChatResponse, SSEEvent } from '@tleblancureta/proto/shared';
2
+ /**
3
+ * Execute a Claude Code turn (blocking, returns full response).
4
+ */
5
+ export declare function runClaude(request: ChatRequest): Promise<ChatResponse>;
6
+ /**
7
+ * Execute a Claude Code turn and yield SSE events.
8
+ */
9
+ export declare function streamClaude(request: ChatRequest): AsyncGenerator<SSEEvent>;
10
+ //# sourceMappingURL=claude-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-runner.d.ts","sourceRoot":"","sources":["../../core-gateway/claude-runner.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AAqEtF;;GAEG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CA2D3E;AAED;;GAEG;AACH,wBAAuB,YAAY,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc,CAAC,QAAQ,CAAC,CAuFlF"}