@harbinger-ai/harbinger 0.1.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 (317) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +406 -0
  3. package/agents/README.md +76 -0
  4. package/agents/_template/CONFIG.yaml +7 -0
  5. package/agents/_template/HEARTBEAT.md +59 -0
  6. package/agents/_template/IDENTITY.md +4 -0
  7. package/agents/_template/SKILLS.md +1 -0
  8. package/agents/_template/SOUL.md +25 -0
  9. package/agents/_template/TOOLS.md +3 -0
  10. package/agents/binary-reverser/CONFIG.yaml +21 -0
  11. package/agents/binary-reverser/HEARTBEAT.md +65 -0
  12. package/agents/binary-reverser/IDENTITY.md +1 -0
  13. package/agents/binary-reverser/SKILLS.md +1 -0
  14. package/agents/binary-reverser/SOUL.md +23 -0
  15. package/agents/binary-reverser/TOOLS.md +99 -0
  16. package/agents/browser-agent/CONFIG.yaml +20 -0
  17. package/agents/browser-agent/HEARTBEAT.md +79 -0
  18. package/agents/browser-agent/IDENTITY.md +5 -0
  19. package/agents/browser-agent/SKILLS.md +86 -0
  20. package/agents/browser-agent/SOUL.md +23 -0
  21. package/agents/browser-agent/TOOLS.md +186 -0
  22. package/agents/cloud-infiltrator/CONFIG.yaml +22 -0
  23. package/agents/cloud-infiltrator/HEARTBEAT.md +78 -0
  24. package/agents/cloud-infiltrator/IDENTITY.md +1 -0
  25. package/agents/cloud-infiltrator/SKILLS.md +1 -0
  26. package/agents/cloud-infiltrator/SOUL.md +23 -0
  27. package/agents/cloud-infiltrator/TOOLS.md +68 -0
  28. package/agents/coding-assistant/CONFIG.yaml +22 -0
  29. package/agents/coding-assistant/HEARTBEAT.md +57 -0
  30. package/agents/coding-assistant/IDENTITY.md +5 -0
  31. package/agents/coding-assistant/SKILLS.md +69 -0
  32. package/agents/coding-assistant/SOUL.md +60 -0
  33. package/agents/coding-assistant/TOOLS.md +168 -0
  34. package/agents/learning-agent/CONFIG.yaml +21 -0
  35. package/agents/learning-agent/HEARTBEAT.md +63 -0
  36. package/agents/learning-agent/IDENTITY.md +5 -0
  37. package/agents/learning-agent/SKILLS.md +86 -0
  38. package/agents/learning-agent/SOUL.md +77 -0
  39. package/agents/learning-agent/TOOLS.md +145 -0
  40. package/agents/maintainer/CONFIG.yaml +31 -0
  41. package/agents/maintainer/HEARTBEAT.md +28 -0
  42. package/agents/maintainer/IDENTITY.md +33 -0
  43. package/agents/maintainer/SKILLS.md +24 -0
  44. package/agents/maintainer/SOUL.md +61 -0
  45. package/agents/maintainer/TOOLS.md +29 -0
  46. package/agents/maintainer/lib/engine.js +279 -0
  47. package/agents/maintainer/lib/safe-fixer.js +183 -0
  48. package/agents/morning-brief/CONFIG.yaml +22 -0
  49. package/agents/morning-brief/HEARTBEAT.md +60 -0
  50. package/agents/morning-brief/IDENTITY.md +5 -0
  51. package/agents/morning-brief/SKILLS.md +56 -0
  52. package/agents/morning-brief/SOUL.md +64 -0
  53. package/agents/morning-brief/TOOLS.md +112 -0
  54. package/agents/osint-detective/CONFIG.yaml +24 -0
  55. package/agents/osint-detective/HEARTBEAT.md +66 -0
  56. package/agents/osint-detective/IDENTITY.md +1 -0
  57. package/agents/osint-detective/SKILLS.md +1 -0
  58. package/agents/osint-detective/SOUL.md +23 -0
  59. package/agents/osint-detective/TOOLS.md +81 -0
  60. package/agents/recon-scout/CONFIG.yaml +22 -0
  61. package/agents/recon-scout/HEARTBEAT.md +79 -0
  62. package/agents/recon-scout/IDENTITY.md +1 -0
  63. package/agents/recon-scout/SKILLS.md +1 -0
  64. package/agents/recon-scout/SOUL.md +23 -0
  65. package/agents/recon-scout/TOOLS.md +93 -0
  66. package/agents/report-writer/CONFIG.yaml +21 -0
  67. package/agents/report-writer/HEARTBEAT.md +63 -0
  68. package/agents/report-writer/IDENTITY.md +1 -0
  69. package/agents/report-writer/SKILLS.md +1 -0
  70. package/agents/report-writer/SOUL.md +23 -0
  71. package/agents/report-writer/TOOLS.md +69 -0
  72. package/agents/shared/README.md +13 -0
  73. package/agents/web-hacker/CONFIG.yaml +24 -0
  74. package/agents/web-hacker/HEARTBEAT.md +78 -0
  75. package/agents/web-hacker/IDENTITY.md +1 -0
  76. package/agents/web-hacker/SKILLS.md +1 -0
  77. package/agents/web-hacker/SOUL.md +23 -0
  78. package/agents/web-hacker/TOOLS.md +86 -0
  79. package/api/CLAUDE.md +19 -0
  80. package/api/index.js +274 -0
  81. package/bin/cli.js +620 -0
  82. package/bin/local.sh +31 -0
  83. package/bin/postinstall.js +63 -0
  84. package/config/index.js +24 -0
  85. package/config/instrumentation.js +93 -0
  86. package/drizzle/0000_initial.sql +52 -0
  87. package/drizzle/0001_bounty_and_registry.sql +82 -0
  88. package/drizzle/0002_sync_columns.sql +7 -0
  89. package/drizzle/0003_graceful_bloodscream.sql +86 -0
  90. package/drizzle/meta/0000_snapshot.json +321 -0
  91. package/drizzle/meta/0003_snapshot.json +878 -0
  92. package/drizzle/meta/_journal.json +34 -0
  93. package/drizzle/relations.ts +3 -0
  94. package/drizzle/schema.ts +145 -0
  95. package/lib/actions.js +47 -0
  96. package/lib/agents.js +166 -0
  97. package/lib/ai/agent.js +96 -0
  98. package/lib/ai/autonomous-engine.js +261 -0
  99. package/lib/ai/index.js +359 -0
  100. package/lib/ai/model-router.js +254 -0
  101. package/lib/ai/model.js +73 -0
  102. package/lib/ai/tools.js +84 -0
  103. package/lib/auth/actions.js +28 -0
  104. package/lib/auth/config.js +27 -0
  105. package/lib/auth/edge-config.js +27 -0
  106. package/lib/auth/index.js +27 -0
  107. package/lib/auth/middleware.js +53 -0
  108. package/lib/bounty/actions.js +119 -0
  109. package/lib/bounty/findings.js +64 -0
  110. package/lib/bounty/programs.js +34 -0
  111. package/lib/bounty/sync-targets.js +267 -0
  112. package/lib/bounty/targets.js +33 -0
  113. package/lib/channels/base.js +56 -0
  114. package/lib/channels/index.js +15 -0
  115. package/lib/channels/telegram.js +148 -0
  116. package/lib/chat/actions.js +288 -0
  117. package/lib/chat/api.js +135 -0
  118. package/lib/chat/components/app-sidebar.js +237 -0
  119. package/lib/chat/components/app-sidebar.jsx +289 -0
  120. package/lib/chat/components/chat-header.js +27 -0
  121. package/lib/chat/components/chat-header.jsx +37 -0
  122. package/lib/chat/components/chat-input.js +230 -0
  123. package/lib/chat/components/chat-input.jsx +228 -0
  124. package/lib/chat/components/chat-nav-context.js +11 -0
  125. package/lib/chat/components/chat-nav-context.jsx +11 -0
  126. package/lib/chat/components/chat-page.js +81 -0
  127. package/lib/chat/components/chat-page.jsx +100 -0
  128. package/lib/chat/components/chat.js +150 -0
  129. package/lib/chat/components/chat.jsx +182 -0
  130. package/lib/chat/components/chats-page.js +302 -0
  131. package/lib/chat/components/chats-page.jsx +330 -0
  132. package/lib/chat/components/crons-page.js +172 -0
  133. package/lib/chat/components/crons-page.jsx +244 -0
  134. package/lib/chat/components/enhanced-tool-call.js +103 -0
  135. package/lib/chat/components/enhanced-tool-call.jsx +139 -0
  136. package/lib/chat/components/findings-page.js +175 -0
  137. package/lib/chat/components/findings-page.jsx +214 -0
  138. package/lib/chat/components/greeting.js +22 -0
  139. package/lib/chat/components/greeting.jsx +26 -0
  140. package/lib/chat/components/icons.js +777 -0
  141. package/lib/chat/components/icons.jsx +741 -0
  142. package/lib/chat/components/index.js +26 -0
  143. package/lib/chat/components/mcp-page.js +260 -0
  144. package/lib/chat/components/mcp-page.jsx +355 -0
  145. package/lib/chat/components/message.js +289 -0
  146. package/lib/chat/components/message.jsx +315 -0
  147. package/lib/chat/components/messages.js +66 -0
  148. package/lib/chat/components/messages.jsx +77 -0
  149. package/lib/chat/components/notifications-page.js +56 -0
  150. package/lib/chat/components/notifications-page.jsx +87 -0
  151. package/lib/chat/components/page-layout.js +21 -0
  152. package/lib/chat/components/page-layout.jsx +28 -0
  153. package/lib/chat/components/registry-page.js +222 -0
  154. package/lib/chat/components/registry-page.jsx +255 -0
  155. package/lib/chat/components/settings-layout.js +40 -0
  156. package/lib/chat/components/settings-layout.jsx +54 -0
  157. package/lib/chat/components/settings-secrets-page.js +216 -0
  158. package/lib/chat/components/settings-secrets-page.jsx +264 -0
  159. package/lib/chat/components/sidebar-history-item.js +132 -0
  160. package/lib/chat/components/sidebar-history-item.jsx +113 -0
  161. package/lib/chat/components/sidebar-history.js +115 -0
  162. package/lib/chat/components/sidebar-history.jsx +157 -0
  163. package/lib/chat/components/sidebar-user-nav.js +63 -0
  164. package/lib/chat/components/sidebar-user-nav.jsx +73 -0
  165. package/lib/chat/components/status-bar.js +39 -0
  166. package/lib/chat/components/status-bar.jsx +51 -0
  167. package/lib/chat/components/swarm-page.js +157 -0
  168. package/lib/chat/components/swarm-page.jsx +210 -0
  169. package/lib/chat/components/targets-page.js +376 -0
  170. package/lib/chat/components/targets-page.jsx +389 -0
  171. package/lib/chat/components/tool-call.js +86 -0
  172. package/lib/chat/components/tool-call.jsx +104 -0
  173. package/lib/chat/components/tool-panel.js +107 -0
  174. package/lib/chat/components/tool-panel.jsx +145 -0
  175. package/lib/chat/components/triggers-page.js +153 -0
  176. package/lib/chat/components/triggers-page.jsx +221 -0
  177. package/lib/chat/components/ui/confirm-dialog.js +53 -0
  178. package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
  179. package/lib/chat/components/ui/dropdown-menu.js +98 -0
  180. package/lib/chat/components/ui/dropdown-menu.jsx +116 -0
  181. package/lib/chat/components/ui/rename-dialog.js +74 -0
  182. package/lib/chat/components/ui/rename-dialog.jsx +72 -0
  183. package/lib/chat/components/ui/scroll-area.js +13 -0
  184. package/lib/chat/components/ui/scroll-area.jsx +17 -0
  185. package/lib/chat/components/ui/separator.js +21 -0
  186. package/lib/chat/components/ui/separator.jsx +18 -0
  187. package/lib/chat/components/ui/sheet.js +75 -0
  188. package/lib/chat/components/ui/sheet.jsx +95 -0
  189. package/lib/chat/components/ui/sidebar.js +227 -0
  190. package/lib/chat/components/ui/sidebar.jsx +245 -0
  191. package/lib/chat/components/ui/tooltip.js +56 -0
  192. package/lib/chat/components/ui/tooltip.jsx +66 -0
  193. package/lib/chat/components/upgrade-dialog.js +151 -0
  194. package/lib/chat/components/upgrade-dialog.jsx +170 -0
  195. package/lib/chat/utils.js +11 -0
  196. package/lib/cron.js +246 -0
  197. package/lib/db/api-keys.js +163 -0
  198. package/lib/db/chats.js +145 -0
  199. package/lib/db/index.js +52 -0
  200. package/lib/db/notifications.js +99 -0
  201. package/lib/db/schema.js +145 -0
  202. package/lib/db/update-check.js +96 -0
  203. package/lib/db/users.js +89 -0
  204. package/lib/mcp/actions.js +104 -0
  205. package/lib/mcp/client.js +79 -0
  206. package/lib/mcp/handler.js +57 -0
  207. package/lib/mcp/server.js +165 -0
  208. package/lib/paths.js +46 -0
  209. package/lib/registry/actions.js +164 -0
  210. package/lib/registry/catalog.js +137 -0
  211. package/lib/registry/tools.js +71 -0
  212. package/lib/tools/create-job.js +99 -0
  213. package/lib/tools/github.js +217 -0
  214. package/lib/tools/openai.js +35 -0
  215. package/lib/tools/telegram.js +292 -0
  216. package/lib/triggers.js +118 -0
  217. package/lib/utils/render-md.js +102 -0
  218. package/package.json +103 -0
  219. package/setup/lib/auth.mjs +81 -0
  220. package/setup/lib/env.mjs +21 -0
  221. package/setup/lib/fs-utils.mjs +20 -0
  222. package/setup/lib/github.mjs +149 -0
  223. package/setup/lib/prerequisites.mjs +155 -0
  224. package/setup/lib/prompts.mjs +267 -0
  225. package/setup/lib/providers.mjs +48 -0
  226. package/setup/lib/sync.mjs +125 -0
  227. package/setup/lib/targets.mjs +45 -0
  228. package/setup/lib/telegram-verify.mjs +63 -0
  229. package/setup/lib/telegram.mjs +76 -0
  230. package/setup/setup-telegram.mjs +264 -0
  231. package/setup/setup.mjs +842 -0
  232. package/templates/.dockerignore +5 -0
  233. package/templates/.env.example +63 -0
  234. package/templates/.github/workflows/auto-merge.yml +117 -0
  235. package/templates/.github/workflows/build-image.yml +36 -0
  236. package/templates/.github/workflows/notify-job-failed.yml +64 -0
  237. package/templates/.github/workflows/notify-pr-complete.yml +119 -0
  238. package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
  239. package/templates/.github/workflows/run-job.yml +89 -0
  240. package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
  241. package/templates/.gitignore.template +45 -0
  242. package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
  243. package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
  244. package/templates/CLAUDE.md +29 -0
  245. package/templates/CLAUDE.md.template +307 -0
  246. package/templates/app/api/[...thepopebot]/route.js +1 -0
  247. package/templates/app/api/auth/[...nextauth]/route.js +1 -0
  248. package/templates/app/chat/[chatId]/page.js +8 -0
  249. package/templates/app/chats/page.js +7 -0
  250. package/templates/app/components/ascii-logo.jsx +10 -0
  251. package/templates/app/components/login-form.jsx +92 -0
  252. package/templates/app/components/setup-form.jsx +82 -0
  253. package/templates/app/components/theme-provider.jsx +11 -0
  254. package/templates/app/components/theme-toggle.jsx +38 -0
  255. package/templates/app/components/ui/button.jsx +21 -0
  256. package/templates/app/components/ui/card.jsx +23 -0
  257. package/templates/app/components/ui/input.jsx +10 -0
  258. package/templates/app/components/ui/label.jsx +10 -0
  259. package/templates/app/crons/page.js +5 -0
  260. package/templates/app/findings/page.js +7 -0
  261. package/templates/app/globals.css +90 -0
  262. package/templates/app/layout.js +19 -0
  263. package/templates/app/login/page.js +15 -0
  264. package/templates/app/notifications/page.js +7 -0
  265. package/templates/app/page.js +7 -0
  266. package/templates/app/settings/crons/page.js +5 -0
  267. package/templates/app/settings/layout.js +7 -0
  268. package/templates/app/settings/mcp/page.js +5 -0
  269. package/templates/app/settings/page.js +5 -0
  270. package/templates/app/settings/secrets/page.js +5 -0
  271. package/templates/app/settings/triggers/page.js +5 -0
  272. package/templates/app/stream/chat/route.js +1 -0
  273. package/templates/app/swarm/page.js +7 -0
  274. package/templates/app/targets/page.js +7 -0
  275. package/templates/app/toolbox/page.js +7 -0
  276. package/templates/app/triggers/page.js +5 -0
  277. package/templates/config/AGENT.md +34 -0
  278. package/templates/config/CRONS.json +56 -0
  279. package/templates/config/EVENT_HANDLER.md +224 -0
  280. package/templates/config/HEARTBEAT.md +3 -0
  281. package/templates/config/JOB_SUMMARY.md +130 -0
  282. package/templates/config/MCP_SERVERS.json +1 -0
  283. package/templates/config/SKILL_BUILDING_GUIDE.md +90 -0
  284. package/templates/config/SOUL.md +17 -0
  285. package/templates/config/TRIGGERS.json +58 -0
  286. package/templates/docker/event-handler/Dockerfile +20 -0
  287. package/templates/docker/event-handler/ecosystem.config.cjs +8 -0
  288. package/templates/docker/job-claude-code/Dockerfile +34 -0
  289. package/templates/docker/job-claude-code/entrypoint.sh +139 -0
  290. package/templates/docker/job-pi-coding-agent/Dockerfile +44 -0
  291. package/templates/docker/job-pi-coding-agent/entrypoint.sh +163 -0
  292. package/templates/docker-compose.yml +63 -0
  293. package/templates/instrumentation.js +6 -0
  294. package/templates/middleware.js +1 -0
  295. package/templates/next.config.mjs +3 -0
  296. package/templates/postcss.config.mjs +5 -0
  297. package/templates/skills/LICENSE +21 -0
  298. package/templates/skills/README.md +119 -0
  299. package/templates/skills/brave-search/SKILL.md +79 -0
  300. package/templates/skills/brave-search/content.js +86 -0
  301. package/templates/skills/brave-search/package-lock.json +621 -0
  302. package/templates/skills/brave-search/package.json +14 -0
  303. package/templates/skills/brave-search/search.js +199 -0
  304. package/templates/skills/browser-tools/SKILL.md +196 -0
  305. package/templates/skills/browser-tools/browser-content.js +103 -0
  306. package/templates/skills/browser-tools/browser-cookies.js +35 -0
  307. package/templates/skills/browser-tools/browser-eval.js +53 -0
  308. package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
  309. package/templates/skills/browser-tools/browser-nav.js +44 -0
  310. package/templates/skills/browser-tools/browser-pick.js +162 -0
  311. package/templates/skills/browser-tools/browser-screenshot.js +34 -0
  312. package/templates/skills/browser-tools/browser-start.js +87 -0
  313. package/templates/skills/browser-tools/package-lock.json +2556 -0
  314. package/templates/skills/browser-tools/package.json +19 -0
  315. package/templates/skills/llm-secrets/SKILL.md +34 -0
  316. package/templates/skills/llm-secrets/llm-secrets.js +33 -0
  317. package/templates/skills/modify-self/SKILL.md +12 -0
@@ -0,0 +1,210 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { PageLayout } from './page-layout.js';
5
+ import { SpinnerIcon, RefreshIcon } from './icons.js';
6
+ import { getSwarmStatus } from '../actions.js';
7
+
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+ // Utilities
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+
12
+ function formatDuration(seconds) {
13
+ if (seconds < 60) return `${seconds}s`;
14
+ const minutes = Math.floor(seconds / 60);
15
+ const secs = seconds % 60;
16
+ if (minutes < 60) return `${minutes}m ${secs}s`;
17
+ const hours = Math.floor(minutes / 60);
18
+ const mins = minutes % 60;
19
+ return `${hours}h ${mins}m`;
20
+ }
21
+
22
+ function timeAgo(timestamp) {
23
+ const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
24
+ if (seconds < 60) return 'just now';
25
+ const minutes = Math.floor(seconds / 60);
26
+ if (minutes < 60) return `${minutes}m ago`;
27
+ const hours = Math.floor(minutes / 60);
28
+ if (hours < 24) return `${hours}h ago`;
29
+ const days = Math.floor(hours / 24);
30
+ if (days < 30) return `${days}d ago`;
31
+ const months = Math.floor(days / 30);
32
+ return `${months}mo ago`;
33
+ }
34
+
35
+ function LoadingSkeleton() {
36
+ return (
37
+ <div className="flex flex-col gap-4">
38
+ {[...Array(5)].map((_, i) => (
39
+ <div key={i} className="h-14 animate-pulse rounded-md bg-border/50" />
40
+ ))}
41
+ </div>
42
+ );
43
+ }
44
+
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+ // Workflow List
47
+ // ─────────────────────────────────────────────────────────────────────────────
48
+
49
+ const conclusionBadgeStyles = {
50
+ success: 'bg-green-500/10 text-green-500',
51
+ failure: 'bg-red-500/10 text-red-500',
52
+ cancelled: 'bg-yellow-500/10 text-yellow-500',
53
+ skipped: 'bg-muted text-muted-foreground',
54
+ };
55
+
56
+ function SwarmWorkflowList({ runs }) {
57
+ if (!runs || runs.length === 0) {
58
+ return (
59
+ <div className="text-sm text-muted-foreground py-4 text-center">
60
+ No workflow runs.
61
+ </div>
62
+ );
63
+ }
64
+
65
+ return (
66
+ <div className="flex flex-col divide-y divide-border">
67
+ {runs.map((run) => {
68
+ const isActive = run.status === 'in_progress' || run.status === 'queued';
69
+ const isRunning = run.status === 'in_progress';
70
+ const isQueued = run.status === 'queued';
71
+
72
+ return (
73
+ <a
74
+ key={run.run_id}
75
+ href={run.html_url}
76
+ target="_blank"
77
+ rel="noopener noreferrer"
78
+ className="flex items-center gap-3 py-3 px-2 -mx-2 rounded-md hover:bg-accent transition-colors no-underline text-inherit"
79
+ >
80
+ {/* Status indicator */}
81
+ {isRunning && (
82
+ <span className="inline-block h-2.5 w-2.5 shrink-0 rounded-full bg-green-500 animate-pulse" />
83
+ )}
84
+ {isQueued && (
85
+ <span className="inline-block h-2.5 w-2.5 shrink-0 rounded-full bg-yellow-500" />
86
+ )}
87
+ {!isActive && (
88
+ <span
89
+ className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium uppercase shrink-0 ${
90
+ conclusionBadgeStyles[run.conclusion] || 'bg-muted text-muted-foreground'
91
+ }`}
92
+ >
93
+ {run.conclusion || 'unknown'}
94
+ </span>
95
+ )}
96
+
97
+ {/* Workflow name */}
98
+ <span className="text-sm font-medium truncate">
99
+ {run.workflow_name || run.branch}
100
+ </span>
101
+
102
+ {/* Duration or time ago */}
103
+ <span className="text-xs text-muted-foreground shrink-0">
104
+ {isActive
105
+ ? formatDuration(run.duration_seconds)
106
+ : timeAgo(run.updated_at || run.started_at)}
107
+ </span>
108
+
109
+ {/* Spacer */}
110
+ <div className="flex-1" />
111
+
112
+ {/* Link label */}
113
+ <span className="text-xs text-blue-500 shrink-0">
114
+ View
115
+ </span>
116
+ </a>
117
+ );
118
+ })}
119
+ </div>
120
+ );
121
+ }
122
+
123
+ // ─────────────────────────────────────────────────────────────────────────────
124
+ // Main Page
125
+ // ─────────────────────────────────────────────────────────────────────────────
126
+
127
+ export function SwarmPage({ session }) {
128
+ const [runs, setRuns] = useState([]);
129
+ const [hasMore, setHasMore] = useState(false);
130
+ const [page, setPage] = useState(1);
131
+ const [loading, setLoading] = useState(true);
132
+ const [refreshing, setRefreshing] = useState(false);
133
+
134
+ const fetchPage = useCallback(async (p) => {
135
+ try {
136
+ const data = await getSwarmStatus(p);
137
+ setRuns(data.runs || []);
138
+ setHasMore(data.hasMore || false);
139
+ setPage(p);
140
+ } catch (err) {
141
+ console.error('Failed to fetch swarm status:', err);
142
+ } finally {
143
+ setLoading(false);
144
+ setRefreshing(false);
145
+ }
146
+ }, []);
147
+
148
+ // Initial load
149
+ useEffect(() => { fetchPage(1); }, [fetchPage]);
150
+
151
+ // Auto-refresh current page every 10s
152
+ useEffect(() => {
153
+ const interval = setInterval(() => fetchPage(page), 10000);
154
+ return () => clearInterval(interval);
155
+ }, [fetchPage, page]);
156
+
157
+ return (
158
+ <PageLayout session={session}>
159
+ {/* Header */}
160
+ <div className="flex items-center justify-between mb-6">
161
+ <h1 className="text-2xl font-semibold">Swarm</h1>
162
+ {!loading && (
163
+ <button
164
+ onClick={() => { setRefreshing(true); fetchPage(1); }}
165
+ disabled={refreshing}
166
+ className="inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium border border-border bg-background text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50 disabled:pointer-events-none"
167
+ >
168
+ {refreshing ? (
169
+ <>
170
+ <SpinnerIcon size={14} />
171
+ Refreshing...
172
+ </>
173
+ ) : (
174
+ <>
175
+ <RefreshIcon size={14} />
176
+ Refresh
177
+ </>
178
+ )}
179
+ </button>
180
+ )}
181
+ </div>
182
+
183
+ {loading ? (
184
+ <LoadingSkeleton />
185
+ ) : (
186
+ <div>
187
+ <SwarmWorkflowList runs={runs} />
188
+ {/* Pagination */}
189
+ <div className="flex items-center justify-between mt-4 pt-4 border-t border-border">
190
+ <button
191
+ onClick={() => { setRefreshing(true); fetchPage(page - 1); }}
192
+ disabled={page <= 1 || refreshing}
193
+ className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium border border-border bg-background text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50 disabled:pointer-events-none"
194
+ >
195
+ Previous
196
+ </button>
197
+ <span className="text-sm text-muted-foreground">Page {page}</span>
198
+ <button
199
+ onClick={() => { setRefreshing(true); fetchPage(page + 1); }}
200
+ disabled={!hasMore || refreshing}
201
+ className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium border border-border bg-background text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50 disabled:pointer-events-none"
202
+ >
203
+ Next
204
+ </button>
205
+ </div>
206
+ </div>
207
+ )}
208
+ </PageLayout>
209
+ );
210
+ }
@@ -0,0 +1,376 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState, useEffect } from "react";
4
+ import { CrosshairIcon, PlusIcon, TrashIcon, ChevronDownIcon, GlobeIcon, SpinnerIcon, DownloadIcon, CheckIcon } from "./icons.js";
5
+ import { getPrograms, createProgram, deleteProgram, getTargets, createTarget, deleteTarget, updateTarget, syncTargetsFromPlatform, syncAllTargets, getSyncStatus } from "../../bounty/actions.js";
6
+ const PLATFORMS = [
7
+ { id: "hackerone", label: "HackerOne", color: "bg-purple-500/10 text-purple-500", border: "border-purple-500/20" },
8
+ { id: "bugcrowd", label: "Bugcrowd", color: "bg-orange-500/10 text-orange-500", border: "border-orange-500/20" },
9
+ { id: "intigriti", label: "Intigriti", color: "bg-blue-500/10 text-blue-500", border: "border-blue-500/20" },
10
+ { id: "yeswehack", label: "YesWeHack", color: "bg-teal-500/10 text-teal-500", border: "border-teal-500/20" },
11
+ { id: "federacy", label: "Federacy", color: "bg-pink-500/10 text-pink-500", border: "border-pink-500/20" },
12
+ { id: "custom", label: "Custom", color: "bg-muted text-muted-foreground", border: "border-border" }
13
+ ];
14
+ const TARGET_TYPES = ["domain", "wildcard", "ip", "cidr", "url", "api", "mobile"];
15
+ const STATUS_COLORS = {
16
+ in_scope: "bg-green-500/10 text-green-500",
17
+ out_of_scope: "bg-red-500/10 text-red-500",
18
+ testing: "bg-yellow-500/10 text-yellow-500",
19
+ completed: "bg-blue-500/10 text-blue-500"
20
+ };
21
+ function timeAgo(ts) {
22
+ if (!ts) return "never";
23
+ const diff = Date.now() - ts;
24
+ const mins = Math.floor(diff / 6e4);
25
+ if (mins < 60) return `${mins}m ago`;
26
+ const hrs = Math.floor(mins / 60);
27
+ if (hrs < 24) return `${hrs}h ago`;
28
+ return `${Math.floor(hrs / 24)}d ago`;
29
+ }
30
+ function SyncPanel({ onSync, syncStatus }) {
31
+ const [syncing, setSyncing] = useState(null);
32
+ const [results, setResults] = useState(null);
33
+ const [maxPrograms, setMaxPrograms] = useState("50");
34
+ async function handleSync(platform) {
35
+ setSyncing(platform);
36
+ setResults(null);
37
+ try {
38
+ const opts = { maxPrograms: parseInt(maxPrograms) || 0 };
39
+ let res;
40
+ if (platform === "all") {
41
+ res = await onSync(null, opts);
42
+ } else {
43
+ res = await onSync(platform, opts);
44
+ }
45
+ setResults(res);
46
+ } catch (err) {
47
+ setResults({ error: err.message });
48
+ }
49
+ setSyncing(null);
50
+ }
51
+ const syncablePlatforms = PLATFORMS.filter((p) => p.id !== "custom");
52
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4 mb-6", children: [
53
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-3", children: [
54
+ /* @__PURE__ */ jsxs("div", { children: [
55
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium", children: "Sync from Bounty Platforms" }),
56
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground mt-0.5", children: "Import programs and targets from bounty-targets-data (arkadiyt/bounty-targets-data)" })
57
+ ] }),
58
+ syncStatus?.lastSyncedAt && /* @__PURE__ */ jsxs("span", { className: "text-[10px] text-muted-foreground", children: [
59
+ "Last sync: ",
60
+ timeAgo(syncStatus.lastSyncedAt)
61
+ ] })
62
+ ] }),
63
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2 mb-3", children: [
64
+ syncablePlatforms.map((p) => {
65
+ const count = syncStatus?.platformCounts?.[p.id] || 0;
66
+ return /* @__PURE__ */ jsxs(
67
+ "button",
68
+ {
69
+ onClick: () => handleSync(p.id),
70
+ disabled: syncing !== null,
71
+ className: `inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium border transition-colors hover:bg-accent/50 disabled:opacity-50 ${p.border}`,
72
+ children: [
73
+ syncing === p.id ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(DownloadIcon, { size: 12 }),
74
+ p.label,
75
+ count > 0 && /* @__PURE__ */ jsx("span", { className: `inline-flex rounded-full px-1.5 py-0.5 text-[9px] ${p.color}`, children: count })
76
+ ]
77
+ },
78
+ p.id
79
+ );
80
+ }),
81
+ /* @__PURE__ */ jsxs(
82
+ "button",
83
+ {
84
+ onClick: () => handleSync("all"),
85
+ disabled: syncing !== null,
86
+ className: "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium bg-foreground text-background hover:opacity-90 disabled:opacity-50",
87
+ children: [
88
+ syncing === "all" ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(DownloadIcon, { size: 12 }),
89
+ "Sync All"
90
+ ]
91
+ }
92
+ )
93
+ ] }),
94
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-3", children: [
95
+ /* @__PURE__ */ jsx("label", { className: "text-[10px] text-muted-foreground", children: "Max programs per platform:" }),
96
+ /* @__PURE__ */ jsx(
97
+ "input",
98
+ {
99
+ type: "number",
100
+ value: maxPrograms,
101
+ onChange: (e) => setMaxPrograms(e.target.value),
102
+ className: "w-20 text-xs border rounded-md px-2 py-1 bg-background",
103
+ min: "0",
104
+ placeholder: "0 = all"
105
+ }
106
+ ),
107
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: "(0 = unlimited)" })
108
+ ] }),
109
+ results && /* @__PURE__ */ jsx("div", { className: "rounded-md bg-muted/50 p-3 mt-2", children: results.error ? /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: results.error }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1", children: Object.entries(results).map(([platform, stats]) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 text-xs", children: [
110
+ /* @__PURE__ */ jsx("span", { className: "font-medium w-24", children: platform }),
111
+ /* @__PURE__ */ jsxs("span", { className: "text-green-500", children: [
112
+ "+",
113
+ stats.programsAdded,
114
+ " programs"
115
+ ] }),
116
+ /* @__PURE__ */ jsxs("span", { className: "text-blue-500", children: [
117
+ stats.programsUpdated,
118
+ " updated"
119
+ ] }),
120
+ /* @__PURE__ */ jsxs("span", { className: "text-green-500", children: [
121
+ "+",
122
+ stats.targetsAdded,
123
+ " targets"
124
+ ] }),
125
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
126
+ stats.targetsSkipped,
127
+ " skipped"
128
+ ] }),
129
+ stats.errors?.length > 0 && /* @__PURE__ */ jsxs("span", { className: "text-destructive", children: [
130
+ stats.errors.length,
131
+ " errors"
132
+ ] })
133
+ ] }, platform)) }) }),
134
+ syncStatus && syncStatus.totalSyncedPrograms > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mt-2 pt-2 border-t", children: [
135
+ /* @__PURE__ */ jsxs("span", { className: "text-[10px] text-muted-foreground", children: [
136
+ syncStatus.totalSyncedPrograms,
137
+ " synced programs:"
138
+ ] }),
139
+ Object.entries(syncStatus.platformCounts || {}).map(([p, count]) => {
140
+ const plat = PLATFORMS.find((x) => x.id === p);
141
+ return /* @__PURE__ */ jsxs("span", { className: `inline-flex rounded-full px-2 py-0.5 text-[9px] font-medium ${plat?.color || ""}`, children: [
142
+ plat?.label || p,
143
+ " (",
144
+ count,
145
+ ")"
146
+ ] }, p);
147
+ })
148
+ ] })
149
+ ] });
150
+ }
151
+ function ProgramCard({ program, onSelect, selected, onDelete }) {
152
+ const platform = PLATFORMS.find((p) => p.id === program.platform) || PLATFORMS[5];
153
+ return /* @__PURE__ */ jsxs(
154
+ "button",
155
+ {
156
+ onClick: () => onSelect(program.id),
157
+ className: `flex items-center gap-3 w-full text-left p-3 rounded-lg border transition-colors ${selected ? "border-foreground bg-accent/50" : "bg-card hover:bg-accent/30"}`,
158
+ children: [
159
+ /* @__PURE__ */ jsx("div", { className: "shrink-0 rounded-md bg-muted p-2", children: /* @__PURE__ */ jsx(GlobeIcon, { size: 14 }) }),
160
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
161
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium truncate", children: program.name }),
162
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-0.5 flex-wrap", children: [
163
+ /* @__PURE__ */ jsx("span", { className: `inline-flex rounded-full px-2 py-0.5 text-[10px] font-medium ${platform.color}`, children: platform.label }),
164
+ program.maxBounty > 0 && /* @__PURE__ */ jsxs("span", { className: "text-[10px] text-muted-foreground", children: [
165
+ "Up to $",
166
+ program.maxBounty.toLocaleString()
167
+ ] }),
168
+ program.syncHandle && /* @__PURE__ */ jsx("span", { className: "inline-flex rounded-full bg-cyan-500/10 px-1.5 py-0.5 text-[9px] text-cyan-500", children: "synced" })
169
+ ] })
170
+ ] }),
171
+ !program.syncHandle && /* @__PURE__ */ jsx("button", { onClick: (e) => {
172
+ e.stopPropagation();
173
+ onDelete(program.id);
174
+ }, className: "shrink-0 p-1 text-muted-foreground hover:text-destructive rounded", children: /* @__PURE__ */ jsx(TrashIcon, { size: 12 }) })
175
+ ]
176
+ }
177
+ );
178
+ }
179
+ function TargetRow({ target, onDelete, onStatusChange }) {
180
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 p-3 rounded-lg border bg-card", children: [
181
+ /* @__PURE__ */ jsx("div", { className: "shrink-0 rounded-md bg-muted p-2", children: /* @__PURE__ */ jsx(CrosshairIcon, { size: 14 }) }),
182
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
183
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-mono font-medium truncate", children: target.value }),
184
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-0.5 flex-wrap", children: [
185
+ /* @__PURE__ */ jsx("span", { className: "inline-flex rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground", children: target.type }),
186
+ target.syncSource && /* @__PURE__ */ jsx("span", { className: "inline-flex rounded-full bg-cyan-500/10 px-1.5 py-0.5 text-[9px] text-cyan-500", children: target.syncSource }),
187
+ target.technologies && /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground truncate", children: JSON.parse(target.technologies).join(", ") })
188
+ ] })
189
+ ] }),
190
+ /* @__PURE__ */ jsxs(
191
+ "select",
192
+ {
193
+ value: target.status,
194
+ onChange: (e) => onStatusChange(target.id, e.target.value),
195
+ className: `text-[10px] font-medium rounded-full px-2 py-0.5 border-0 cursor-pointer ${STATUS_COLORS[target.status] || ""}`,
196
+ children: [
197
+ /* @__PURE__ */ jsx("option", { value: "in_scope", children: "in scope" }),
198
+ /* @__PURE__ */ jsx("option", { value: "testing", children: "testing" }),
199
+ /* @__PURE__ */ jsx("option", { value: "completed", children: "completed" }),
200
+ /* @__PURE__ */ jsx("option", { value: "out_of_scope", children: "out of scope" })
201
+ ]
202
+ }
203
+ ),
204
+ /* @__PURE__ */ jsx("button", { onClick: () => onDelete(target.id), className: "shrink-0 p-1 text-muted-foreground hover:text-destructive rounded", children: /* @__PURE__ */ jsx(TrashIcon, { size: 12 }) })
205
+ ] });
206
+ }
207
+ function AddForm({ fields, onSubmit }) {
208
+ const [values, setValues] = useState({});
209
+ const [open, setOpen] = useState(false);
210
+ if (!open) return /* @__PURE__ */ jsxs("button", { onClick: () => setOpen(true), className: "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium border border-dashed hover:bg-accent/50 transition-colors text-muted-foreground hover:text-foreground", children: [
211
+ /* @__PURE__ */ jsx(PlusIcon, { size: 12 }),
212
+ " Add"
213
+ ] });
214
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-end gap-2 p-3 rounded-lg border bg-card", children: [
215
+ fields.map((f) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
216
+ /* @__PURE__ */ jsx("label", { className: "text-[10px] text-muted-foreground", children: f.label }),
217
+ f.type === "select" ? /* @__PURE__ */ jsx("select", { value: values[f.name] || "", onChange: (e) => setValues({ ...values, [f.name]: e.target.value }), className: "text-xs border rounded-md px-2 py-1.5 bg-background", children: f.options.map((o) => /* @__PURE__ */ jsx("option", { value: o, children: o }, o)) }) : /* @__PURE__ */ jsx("input", { type: f.type || "text", placeholder: f.placeholder, value: values[f.name] || "", onChange: (e) => setValues({ ...values, [f.name]: e.target.value }), className: "text-xs border rounded-md px-2 py-1.5 bg-background min-w-[120px]" })
218
+ ] }, f.name)),
219
+ /* @__PURE__ */ jsx("button", { onClick: () => {
220
+ onSubmit(values);
221
+ setValues({});
222
+ setOpen(false);
223
+ }, className: "inline-flex items-center gap-1 rounded-md px-3 py-1.5 text-xs font-medium bg-foreground text-background hover:opacity-90", children: "Save" }),
224
+ /* @__PURE__ */ jsx("button", { onClick: () => {
225
+ setValues({});
226
+ setOpen(false);
227
+ }, className: "text-xs text-muted-foreground hover:text-foreground px-2 py-1.5", children: "Cancel" })
228
+ ] });
229
+ }
230
+ function TargetsPage() {
231
+ const [programs_, setPrograms] = useState([]);
232
+ const [targets_, setTargets] = useState([]);
233
+ const [selectedProgram, setSelectedProgram] = useState(null);
234
+ const [loading, setLoading] = useState(true);
235
+ const [syncStatus, setSyncStatus] = useState(null);
236
+ const [platformFilter, setPlatformFilter] = useState("all");
237
+ async function load() {
238
+ const [p, ss] = await Promise.all([getPrograms(), getSyncStatus()]);
239
+ setPrograms(p);
240
+ setSyncStatus(ss);
241
+ if (p.length > 0 && !selectedProgram) setSelectedProgram(p[0].id);
242
+ setLoading(false);
243
+ }
244
+ async function loadTargets() {
245
+ if (!selectedProgram) {
246
+ setTargets([]);
247
+ return;
248
+ }
249
+ const t = await getTargets(selectedProgram);
250
+ setTargets(t);
251
+ }
252
+ useEffect(() => {
253
+ load();
254
+ }, []);
255
+ useEffect(() => {
256
+ if (selectedProgram) loadTargets();
257
+ }, [selectedProgram]);
258
+ async function handleSync(platform, options) {
259
+ let res;
260
+ if (platform) {
261
+ const stats = await syncTargetsFromPlatform(platform, options);
262
+ res = { [platform]: stats };
263
+ } else {
264
+ res = await syncAllTargets(options);
265
+ }
266
+ await load();
267
+ if (selectedProgram) loadTargets();
268
+ return res;
269
+ }
270
+ async function handleAddProgram(values) {
271
+ await createProgram({ name: values.name || "Untitled", platform: values.platform || "custom", url: values.url, maxBounty: values.maxBounty ? Number(values.maxBounty) : null });
272
+ load();
273
+ }
274
+ async function handleDeleteProgram(id) {
275
+ await deleteProgram(id);
276
+ if (selectedProgram === id) setSelectedProgram(null);
277
+ load();
278
+ }
279
+ async function handleAddTarget(values) {
280
+ await createTarget({ programId: selectedProgram, type: values.type || "domain", value: values.value || "" });
281
+ loadTargets();
282
+ }
283
+ async function handleDeleteTarget(id) {
284
+ await deleteTarget(id);
285
+ loadTargets();
286
+ }
287
+ async function handleStatusChange(id, status) {
288
+ await updateTarget(id, { status });
289
+ loadTargets();
290
+ }
291
+ const filteredPrograms = platformFilter === "all" ? programs_ : programs_.filter((p) => p.platform === platformFilter);
292
+ if (loading) return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: [...Array(3)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-16 animate-pulse rounded-lg bg-border/50" }, i)) });
293
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
294
+ /* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
295
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Targets" }),
296
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mt-1", children: [
297
+ programs_.length,
298
+ " program",
299
+ programs_.length !== 1 ? "s" : "",
300
+ ", ",
301
+ targets_.length,
302
+ " target",
303
+ targets_.length !== 1 ? "s" : "",
304
+ " in scope"
305
+ ] })
306
+ ] }),
307
+ /* @__PURE__ */ jsx(SyncPanel, { onSync: handleSync, syncStatus }),
308
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1 mb-4 overflow-x-auto pb-1", children: [
309
+ /* @__PURE__ */ jsxs("button", { onClick: () => setPlatformFilter("all"), className: `shrink-0 px-3 py-1 rounded-full text-xs font-medium transition-colors ${platformFilter === "all" ? "bg-foreground text-background" : "bg-muted text-muted-foreground hover:text-foreground"}`, children: [
310
+ "All (",
311
+ programs_.length,
312
+ ")"
313
+ ] }),
314
+ PLATFORMS.filter((p) => programs_.some((prog) => prog.platform === p.id)).map((p) => /* @__PURE__ */ jsxs("button", { onClick: () => setPlatformFilter(p.id), className: `shrink-0 px-3 py-1 rounded-full text-xs font-medium transition-colors ${platformFilter === p.id ? "bg-foreground text-background" : "bg-muted text-muted-foreground hover:text-foreground"}`, children: [
315
+ p.label,
316
+ " (",
317
+ programs_.filter((prog) => prog.platform === p.id).length,
318
+ ")"
319
+ ] }, p.id))
320
+ ] }),
321
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-3 gap-6", children: [
322
+ /* @__PURE__ */ jsxs("div", { className: "lg:col-span-1", children: [
323
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-3", children: [
324
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-medium", children: "Programs" }),
325
+ /* @__PURE__ */ jsx(
326
+ AddForm,
327
+ {
328
+ fields: [
329
+ { name: "name", label: "Name", placeholder: "Program name" },
330
+ { name: "platform", label: "Platform", type: "select", options: ["hackerone", "bugcrowd", "intigriti", "yeswehack", "federacy", "custom"] },
331
+ { name: "url", label: "URL", placeholder: "https://..." },
332
+ { name: "maxBounty", label: "Max Bounty", placeholder: "10000", type: "number" }
333
+ ],
334
+ onSubmit: handleAddProgram
335
+ }
336
+ )
337
+ ] }),
338
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 max-h-[600px] overflow-y-auto", children: filteredPrograms.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center py-8 text-center", children: [
339
+ /* @__PURE__ */ jsx("div", { className: "rounded-full bg-muted p-3 mb-3", children: /* @__PURE__ */ jsx(GlobeIcon, { size: 20 }) }),
340
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "No programs yet" }),
341
+ /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-1", children: "Sync from platforms or add manually" })
342
+ ] }) : filteredPrograms.map((p) => /* @__PURE__ */ jsx(ProgramCard, { program: p, selected: selectedProgram === p.id, onSelect: setSelectedProgram, onDelete: handleDeleteProgram }, p.id)) })
343
+ ] }),
344
+ /* @__PURE__ */ jsxs("div", { className: "lg:col-span-2", children: [
345
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-3", children: [
346
+ /* @__PURE__ */ jsxs("h2", { className: "text-sm font-medium", children: [
347
+ "Targets ",
348
+ selectedProgram && `\u2014 ${programs_.find((p) => p.id === selectedProgram)?.name || ""}`
349
+ ] }),
350
+ selectedProgram && /* @__PURE__ */ jsx(
351
+ AddForm,
352
+ {
353
+ fields: [
354
+ { name: "value", label: "Target", placeholder: "*.example.com" },
355
+ { name: "type", label: "Type", type: "select", options: TARGET_TYPES }
356
+ ],
357
+ onSubmit: handleAddTarget
358
+ }
359
+ )
360
+ ] }),
361
+ !selectedProgram ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center py-12 text-center", children: [
362
+ /* @__PURE__ */ jsx("div", { className: "rounded-full bg-muted p-4 mb-4", children: /* @__PURE__ */ jsx(CrosshairIcon, { size: 24 }) }),
363
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium mb-1", children: "Select a program" }),
364
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Choose a program to manage its targets" })
365
+ ] }) : targets_.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center py-12 text-center", children: [
366
+ /* @__PURE__ */ jsx("div", { className: "rounded-full bg-muted p-4 mb-4", children: /* @__PURE__ */ jsx(CrosshairIcon, { size: 24 }) }),
367
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium mb-1", children: "No targets yet" }),
368
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Add domains, IPs, and URLs to start hunting" })
369
+ ] }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 max-h-[600px] overflow-y-auto", children: targets_.map((t) => /* @__PURE__ */ jsx(TargetRow, { target: t, onDelete: handleDeleteTarget, onStatusChange: handleStatusChange }, t.id)) })
370
+ ] })
371
+ ] })
372
+ ] });
373
+ }
374
+ export {
375
+ TargetsPage
376
+ };