@flame0510/project-aether 1.1.9

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 (290) hide show
  1. package/README.md +150 -0
  2. package/agent-templates/README.md +37 -0
  3. package/agent-templates/argus/AGENTS.md +52 -0
  4. package/agent-templates/argus/IDENTITY.md +7 -0
  5. package/agent-templates/argus/MEMORY.md +9 -0
  6. package/agent-templates/argus/SOUL.md +24 -0
  7. package/agent-templates/argus/TOOLS.md +8 -0
  8. package/agent-templates/atlas/AGENTS.md +47 -0
  9. package/agent-templates/atlas/HEARTBEAT.md +10 -0
  10. package/agent-templates/atlas/IDENTITY.md +6 -0
  11. package/agent-templates/atlas/MEMORY.md +9 -0
  12. package/agent-templates/atlas/SOUL.md +21 -0
  13. package/agent-templates/atlas/TOOLS.md +4 -0
  14. package/agent-templates/base-image/.trigger +0 -0
  15. package/agent-templates/base-image/Dockerfile +90 -0
  16. package/agent-templates/base-image/entrypoint.sh +37 -0
  17. package/agent-templates/custom/AGENTS.md +29 -0
  18. package/agent-templates/custom/BOOTSTRAP.md +56 -0
  19. package/agent-templates/custom/MEMORY.md +8 -0
  20. package/agent-templates/prometheus/AGENTS.md +37 -0
  21. package/agent-templates/prometheus/IDENTITY.md +6 -0
  22. package/agent-templates/prometheus/MEMORY.md +9 -0
  23. package/agent-templates/prometheus/SOUL.md +21 -0
  24. package/agent-templates/prometheus/TOOLS.md +4 -0
  25. package/app/agents/ChannelManager.tsx +465 -0
  26. package/app/agents/ImageDownloadBanner.tsx +159 -0
  27. package/app/agents/PageClient.tsx +1285 -0
  28. package/app/agents/create/PageClient.tsx +437 -0
  29. package/app/agents/create/page.tsx +126 -0
  30. package/app/agents/loading.tsx +14 -0
  31. package/app/agents/page.tsx +5 -0
  32. package/app/api/agents/[id]/backup/route.ts +169 -0
  33. package/app/api/agents/[id]/channels/pairing/route.ts +69 -0
  34. package/app/api/agents/[id]/channels/route.ts +14 -0
  35. package/app/api/agents/[id]/channels/telegram/route.ts +50 -0
  36. package/app/api/agents/[id]/lifecycle/route.ts +94 -0
  37. package/app/api/agents/[id]/recreate/route.ts +199 -0
  38. package/app/api/agents/[id]/restart/route.ts +33 -0
  39. package/app/api/agents/[id]/restore/route.ts +81 -0
  40. package/app/api/agents/[id]/route.ts +280 -0
  41. package/app/api/agents/channels-summary/route.ts +39 -0
  42. package/app/api/agents/config-json/route.ts +58 -0
  43. package/app/api/agents/create/route.ts +488 -0
  44. package/app/api/agents/download-image/route.ts +32 -0
  45. package/app/api/agents/image-status/route.ts +126 -0
  46. package/app/api/agents/route.ts +213 -0
  47. package/app/api/agents/token/route.ts +94 -0
  48. package/app/api/agents-active/route.ts +190 -0
  49. package/app/api/agents-config/route.ts +629 -0
  50. package/app/api/alerts/smoke/route.ts +26 -0
  51. package/app/api/aliases/add/route.ts +28 -0
  52. package/app/api/aliases/remove/route.ts +27 -0
  53. package/app/api/assistant/route.ts +204 -0
  54. package/app/api/auth/check/route.ts +38 -0
  55. package/app/api/auth/login/route.ts +85 -0
  56. package/app/api/auth/logout/route.ts +7 -0
  57. package/app/api/auth/status/route.ts +33 -0
  58. package/app/api/config/env/route.ts +132 -0
  59. package/app/api/config/restart/route.ts +26 -0
  60. package/app/api/containers/route.ts +75 -0
  61. package/app/api/cost-override/route.ts +46 -0
  62. package/app/api/costs/route.ts +84 -0
  63. package/app/api/credentials/[id]/reveal/route.ts +21 -0
  64. package/app/api/credentials/[id]/route.ts +52 -0
  65. package/app/api/credentials/[id]/sync/route.ts +58 -0
  66. package/app/api/credentials/detect/route.ts +246 -0
  67. package/app/api/credentials/route.ts +33 -0
  68. package/app/api/crons/[id]/route.ts +69 -0
  69. package/app/api/crons/route.ts +12 -0
  70. package/app/api/crons/runs/route.ts +31 -0
  71. package/app/api/debug/route.ts +11 -0
  72. package/app/api/envcheck/route.ts +18 -0
  73. package/app/api/events/route.ts +24 -0
  74. package/app/api/gateway/agent/route.ts +154 -0
  75. package/app/api/gateway/provider/balance/route.ts +38 -0
  76. package/app/api/gateway/provider/keys.ts +38 -0
  77. package/app/api/gateway/provider/oauth/route.ts +304 -0
  78. package/app/api/gateway/provider/route.ts +209 -0
  79. package/app/api/gateway/route.ts +223 -0
  80. package/app/api/gateway/sync.ts +189 -0
  81. package/app/api/lineage/route.ts +54 -0
  82. package/app/api/memory-context/route.ts +15 -0
  83. package/app/api/metrics/route.ts +35 -0
  84. package/app/api/models/route.ts +77 -0
  85. package/app/api/plugins/route.js +37 -0
  86. package/app/api/provider/upstream.ts +142 -0
  87. package/app/api/provider/v1/chat/completions/route.ts +146 -0
  88. package/app/api/provider/v1/models/route.ts +167 -0
  89. package/app/api/provider-usage/route.ts +629 -0
  90. package/app/api/session/route.ts +39 -0
  91. package/app/api/sessions/[id]/route.ts +38 -0
  92. package/app/api/sessions/route.ts +23 -0
  93. package/app/api/setup/agent-image/route.ts +102 -0
  94. package/app/api/setup/password/route.ts +111 -0
  95. package/app/api/setup/restart/route.ts +76 -0
  96. package/app/api/skills/delete/route.js +105 -0
  97. package/app/api/skills/promote/route.js +129 -0
  98. package/app/api/skills/route.js +203 -0
  99. package/app/api/skills/save/route.js +142 -0
  100. package/app/api/stats/route.ts +34 -0
  101. package/app/api/stats-since/route.ts +20 -0
  102. package/app/api/stream/route.ts +70 -0
  103. package/app/api/system-health/route.ts +233 -0
  104. package/app/api/tool-calls/route.ts +24 -0
  105. package/app/api/tools-config/route.ts +163 -0
  106. package/app/api/update-check/route.ts +127 -0
  107. package/app/api/version/route.ts +25 -0
  108. package/app/api/wizard/complete/route.ts +32 -0
  109. package/app/api/wizard/reset/route.ts +28 -0
  110. package/app/api/wizard/status/route.ts +28 -0
  111. package/app/api/workspace/route.ts +392 -0
  112. package/app/api/workspace/stream/route.ts +136 -0
  113. package/app/components/AppShell.tsx +33 -0
  114. package/app/components/AuthGuard.tsx +91 -0
  115. package/app/components/CostBreakdown.tsx +37 -0
  116. package/app/components/DashboardHeader.tsx +73 -0
  117. package/app/components/DashboardLayout.tsx +76 -0
  118. package/app/components/DashboardToolbar.tsx +61 -0
  119. package/app/components/FloatingActions.tsx +33 -0
  120. package/app/components/Icons.tsx +78 -0
  121. package/app/components/LineageGraphPage.tsx +142 -0
  122. package/app/components/LiveFeed.tsx +48 -0
  123. package/app/components/MobileBottomNav.tsx +84 -0
  124. package/app/components/ModelPickerModal.tsx +94 -0
  125. package/app/components/PasswordInput.tsx +86 -0
  126. package/app/components/PulseChat.tsx +362 -0
  127. package/app/components/Rev4aLoader.tsx +48 -0
  128. package/app/components/SessionDrawer.tsx +263 -0
  129. package/app/components/SessionTopology.tsx +289 -0
  130. package/app/components/Sidebar.tsx +294 -0
  131. package/app/components/Skeleton.tsx +133 -0
  132. package/app/components/SystemCockpit.tsx +169 -0
  133. package/app/components/VersionBanner.tsx +105 -0
  134. package/app/components/WizardButton.tsx +30 -0
  135. package/app/components/ui/Badge.tsx +30 -0
  136. package/app/components/ui/Button.tsx +59 -0
  137. package/app/components/ui/ConfirmModal.tsx +46 -0
  138. package/app/components/ui/FilterBar.tsx +33 -0
  139. package/app/components/ui/Input.tsx +25 -0
  140. package/app/components/ui/ItemList.tsx +83 -0
  141. package/app/components/ui/LoadingSpinner.tsx +30 -0
  142. package/app/components/ui/Metric.tsx +22 -0
  143. package/app/components/ui/Modal.tsx +42 -0
  144. package/app/components/ui/Page.tsx +18 -0
  145. package/app/components/ui/Pill.tsx +6 -0
  146. package/app/components/ui/PropertyList.tsx +24 -0
  147. package/app/components/ui/Select.tsx +26 -0
  148. package/app/components/ui/StatusCard.tsx +32 -0
  149. package/app/components/ui/Surface.tsx +14 -0
  150. package/app/components/ui/Tabs.tsx +55 -0
  151. package/app/components/ui/TemplateOption.tsx +59 -0
  152. package/app/components/ui/Toast.tsx +44 -0
  153. package/app/components/ui/index.ts +19 -0
  154. package/app/components/ui/tokens.ts +18 -0
  155. package/app/config/PageClient.tsx +293 -0
  156. package/app/config/loading.tsx +10 -0
  157. package/app/config/page.tsx +7 -0
  158. package/app/containers/ContainersClient.tsx +129 -0
  159. package/app/containers/loading.tsx +21 -0
  160. package/app/containers/page.tsx +5 -0
  161. package/app/containers/terminal/[id]/TerminalClient.tsx +313 -0
  162. package/app/containers/terminal/[id]/page.tsx +6 -0
  163. package/app/credentials/PageClient.tsx +598 -0
  164. package/app/credentials/loading.tsx +10 -0
  165. package/app/credentials/page.tsx +7 -0
  166. package/app/crons/PageClient.tsx +538 -0
  167. package/app/crons/loading.tsx +10 -0
  168. package/app/crons/page.tsx +5 -0
  169. package/app/design-system.ts +57 -0
  170. package/app/gateway/PageClient.tsx +1048 -0
  171. package/app/gateway/loading.tsx +24 -0
  172. package/app/gateway/page.tsx +5 -0
  173. package/app/globals.css +1474 -0
  174. package/app/layout.tsx +45 -0
  175. package/app/lib/models-context.tsx +41 -0
  176. package/app/lineage/loading.tsx +10 -0
  177. package/app/lineage/page.tsx +13 -0
  178. package/app/loading.tsx +16 -0
  179. package/app/login/page.tsx +160 -0
  180. package/app/memory/MemoryContextPageClient.tsx +196 -0
  181. package/app/memory/loading.tsx +10 -0
  182. package/app/memory/page.tsx +8 -0
  183. package/app/page.tsx +31 -0
  184. package/app/plugins/PageClient.tsx +296 -0
  185. package/app/plugins/loading.tsx +10 -0
  186. package/app/plugins/page.tsx +5 -0
  187. package/app/setup/PageClient.tsx +227 -0
  188. package/app/setup/loading.tsx +21 -0
  189. package/app/setup/page.tsx +7 -0
  190. package/app/skills/PageClient.tsx +499 -0
  191. package/app/skills/loading.tsx +10 -0
  192. package/app/skills/page.tsx +5 -0
  193. package/app/tools/ToolsPageClient.tsx +308 -0
  194. package/app/tools/loading.tsx +10 -0
  195. package/app/tools/page.tsx +62 -0
  196. package/app/tools/tools-catalog.ts +141 -0
  197. package/app/wizard/PageClient.tsx +485 -0
  198. package/app/wizard/icons.tsx +89 -0
  199. package/app/wizard/loading.tsx +24 -0
  200. package/app/wizard/page.tsx +7 -0
  201. package/app/wizard/storage.ts +33 -0
  202. package/app/wizard/useWizard.ts +101 -0
  203. package/app/workspace/WorkspaceClient.tsx +633 -0
  204. package/app/workspace/loading.tsx +36 -0
  205. package/app/workspace/page.tsx +8 -0
  206. package/bin/docker-entrypoint.sh +21 -0
  207. package/bin/postinstall.js +20 -0
  208. package/bin/rev4a.js +696 -0
  209. package/daemon.js +783 -0
  210. package/docs/ARCHITECTURE.md +557 -0
  211. package/docs/CONTAINER-TERMINAL.md +280 -0
  212. package/docs/DESIGN-SYSTEM.md +99 -0
  213. package/docs/FRONTEND-ARCHITECTURE.md +119 -0
  214. package/docs/REV4A.md +504 -0
  215. package/docs/dev/API-REFERENCE.md +1835 -0
  216. package/docs/dev/DATABASE.md +224 -0
  217. package/docs/dev/GATEWAY.md +300 -0
  218. package/docs/dev/PROVIDERS.md +229 -0
  219. package/docs/dev/SESSION-MAINTENANCE-PLAN.md +607 -0
  220. package/docs/dev/WORKSPACE.md +199 -0
  221. package/docs/rag/DATA-FRESHNESS.md +109 -0
  222. package/docs/rag/GLOSSARY.md +125 -0
  223. package/docs/rag/REV4A-OVERVIEW.md +135 -0
  224. package/docs/rag/WHAT-I-CAN-ANSWER.md +124 -0
  225. package/lib/agent-setup.ts +130 -0
  226. package/lib/alerts.ts +115 -0
  227. package/lib/apiFetch.ts +7 -0
  228. package/lib/auth.ts +28 -0
  229. package/lib/billing.ts +100 -0
  230. package/lib/buildAgentImage.ts +346 -0
  231. package/lib/channelManager.ts +386 -0
  232. package/lib/container.ts +60 -0
  233. package/lib/credentials/db.ts +86 -0
  234. package/lib/credentials/delivery.ts +448 -0
  235. package/lib/credentials/detect.ts +149 -0
  236. package/lib/credentials/hash.ts +9 -0
  237. package/lib/credentials/providers.ts +110 -0
  238. package/lib/credentials/vault.ts +195 -0
  239. package/lib/db-bootstrap.d.ts +3 -0
  240. package/lib/db-bootstrap.mjs +144 -0
  241. package/lib/db.ts +25 -0
  242. package/lib/docker-utils.ts +60 -0
  243. package/lib/hooks/useDashboard.ts +264 -0
  244. package/lib/hooks/useRev4aTimezone.ts +28 -0
  245. package/lib/hooks/useScreenshot.ts +30 -0
  246. package/lib/memory-context.ts +385 -0
  247. package/lib/model-catalogue.ts +131 -0
  248. package/lib/model-pricing.ts +63 -0
  249. package/lib/openclaw-cron.ts +350 -0
  250. package/lib/patterns/ApiAdapter.ts +73 -0
  251. package/lib/patterns/ApiClient.ts +65 -0
  252. package/lib/patterns/EventBus.ts +113 -0
  253. package/lib/patterns/FilterStrategy.ts +79 -0
  254. package/lib/patterns/SessionFactory.ts +117 -0
  255. package/lib/patterns/sessionPresentation.ts +117 -0
  256. package/lib/provider-balance.ts +163 -0
  257. package/lib/requireAuth.tsx +38 -0
  258. package/lib/rev4a-auth.d.ts +3 -0
  259. package/lib/rev4a-auth.js +36 -0
  260. package/lib/rev4a-paths.ts +145 -0
  261. package/lib/timezone.ts +46 -0
  262. package/lib/types/index.ts +96 -0
  263. package/lib/utils/format.ts +82 -0
  264. package/lineage.js +56 -0
  265. package/model-pricing.json +482 -0
  266. package/models.config.json +844 -0
  267. package/next-env.d.ts +6 -0
  268. package/next.config.mjs +7 -0
  269. package/package.json +82 -0
  270. package/public/apple-touch-icon.png +0 -0
  271. package/public/favicon-64.png +0 -0
  272. package/public/favicon.svg +58 -0
  273. package/public/icon-192-maskable.png +0 -0
  274. package/public/icon-192.png +0 -0
  275. package/public/icon-512-maskable.png +0 -0
  276. package/public/icon-512.png +0 -0
  277. package/public/manifest.json +38 -0
  278. package/public/rev4a-logo.png +0 -0
  279. package/public/rev4a-logo.svg +58 -0
  280. package/rev4a-rules/AGENTS.md +37 -0
  281. package/scripts/add-auth.cjs +185 -0
  282. package/scripts/backup.sh +66 -0
  283. package/scripts/check-providers.mjs +143 -0
  284. package/scripts/docker/traefik/docker-compose.yml +32 -0
  285. package/scripts/docker/traefik/dynamic/.gitkeep +18 -0
  286. package/scripts/docker/traefik/traefik.yml +37 -0
  287. package/scripts/remove-auth-profile.js +56 -0
  288. package/scripts/restore.sh +82 -0
  289. package/terminal-ws-server.js +139 -0
  290. package/tsconfig.json +42 -0
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+
5
+ interface TabItem {
6
+ label: string;
7
+ active: boolean;
8
+ onClick: () => void;
9
+ count?: number | string;
10
+ }
11
+
12
+ interface TabsProps {
13
+ items: TabItem[];
14
+ className?: string;
15
+ }
16
+
17
+ export function Tabs({ items, className }: TabsProps) {
18
+ return (
19
+ <div
20
+ style={{
21
+ display: 'flex',
22
+ gap: 0,
23
+ borderBottom: '1px solid var(--border)',
24
+ marginBottom: 20,
25
+ }}
26
+ className={className}
27
+ >
28
+ {items.map((item) => (
29
+ <button
30
+ type="button"
31
+ key={item.label}
32
+ onClick={item.onClick}
33
+ style={{
34
+ background: 'transparent',
35
+ border: 'none',
36
+ borderBottom: item.active ? '2px solid var(--violet)' : '2px solid transparent',
37
+ color: item.active ? 'var(--violet)' : 'var(--text-dim)',
38
+ padding: '12px 16px',
39
+ fontSize: 13,
40
+ fontFamily: 'var(--font-mono-stack)',
41
+ cursor: 'pointer',
42
+ letterSpacing: '0.04em',
43
+ transition: 'color 0.12s, border-color 0.12s',
44
+ flex: 1,
45
+ fontWeight: item.active ? 600 : 400,
46
+ textTransform: 'capitalize',
47
+ }}
48
+ >
49
+ {item.label}
50
+ {item.count !== undefined ? ` (${item.count})` : ''}
51
+ </button>
52
+ ))}
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+
5
+ interface TemplateOptionProps {
6
+ id: string;
7
+ selected: boolean;
8
+ description: string;
9
+ onClick: () => void;
10
+ badge?: ReactNode;
11
+ }
12
+
13
+ export function TemplateOption({ id, selected, description, onClick, badge }: TemplateOptionProps) {
14
+ return (
15
+ <button
16
+ type="button"
17
+ onClick={onClick}
18
+ style={{
19
+ display: 'flex',
20
+ alignItems: 'center',
21
+ gap: 12,
22
+ width: '100%',
23
+ padding: '14px 16px',
24
+ textAlign: 'left',
25
+ fontSize: 14,
26
+ fontFamily: 'var(--font-mono-stack)',
27
+ color: 'var(--text)',
28
+ cursor: 'pointer',
29
+ outline: 'none',
30
+ transition: 'background 0.1s, border-color 0.1s',
31
+ background: selected ? 'var(--violet-bg)' : 'transparent',
32
+ border: `1px solid ${selected ? 'var(--violet)' : 'var(--border)'}`,
33
+ }}
34
+ >
35
+ <span
36
+ style={{
37
+ width: 28,
38
+ height: 28,
39
+ borderRadius: '50%',
40
+ display: 'flex',
41
+ alignItems: 'center',
42
+ justifyContent: 'center',
43
+ background: selected ? 'var(--violet)' : 'var(--border)',
44
+ color: selected ? '#111' : '#666',
45
+ fontSize: 12,
46
+ fontWeight: 600,
47
+ flexShrink: 0,
48
+ }}
49
+ >
50
+ {id[0].toUpperCase()}
51
+ </span>
52
+ <div style={{ flex: 1, minWidth: 0 }}>
53
+ <div style={{ fontWeight: 500, fontSize: 14 }}>{id}</div>
54
+ <div style={{ fontSize: 11, color: '#888', marginTop: 2 }}>{description}</div>
55
+ </div>
56
+ {badge}
57
+ </button>
58
+ );
59
+ }
@@ -0,0 +1,44 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ type ToastType = 'success' | 'error' | 'info';
6
+
7
+ interface ToastItem {
8
+ id: number;
9
+ message: string;
10
+ type: ToastType;
11
+ }
12
+
13
+ let nextId = 0;
14
+ const listeners: Set<(t: ToastItem) => void> = new Set();
15
+
16
+ export function showToast(message: string, type: ToastType = 'info') {
17
+ const item: ToastItem = { id: nextId++, message, type };
18
+ listeners.forEach((fn) => fn(item));
19
+ }
20
+
21
+ export function ToastContainer() {
22
+ const [toasts, setToasts] = useState<ToastItem[]>([]);
23
+
24
+ useEffect(() => {
25
+ const handler = (t: ToastItem) => {
26
+ setToasts((prev) => [...prev, t]);
27
+ setTimeout(() => {
28
+ setToasts((prev) => prev.filter((x) => x.id !== t.id));
29
+ }, 4000);
30
+ };
31
+ listeners.add(handler);
32
+ return () => { listeners.delete(handler); };
33
+ }, []);
34
+
35
+ return (
36
+ <div className="ui-toast-container">
37
+ {toasts.map((t) => (
38
+ <div key={t.id} className={`ui-toast ui-toast--${t.type}`}>
39
+ {t.message}
40
+ </div>
41
+ ))}
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,19 @@
1
+ export * from './ConfirmModal';
2
+ export * from './Badge';
3
+ export * from './Button';
4
+ export * from './FilterBar';
5
+ export * from './Input';
6
+ export * from './LoadingSpinner';
7
+ export * from './Metric';
8
+ export * from './Modal';
9
+ export * from './Page';
10
+ export * from './Pill';
11
+ export * from './PropertyList';
12
+ export * from './Select';
13
+ export * from './StatusCard';
14
+ export * from './Surface';
15
+ export * from './Toast';
16
+ export * from './tokens';
17
+ export * from './Tabs';
18
+ export * from './ItemList';
19
+ export * from './TemplateOption';
@@ -0,0 +1,18 @@
1
+ export type Tone = 'neutral' | 'accent' | 'success' | 'warning' | 'danger' | 'info';
2
+ export type SurfaceVariant = 'panel' | 'card' | 'subtle';
3
+
4
+ export const toneVars: Record<Tone, { text: string; border: string; bg: string }> = {
5
+ neutral: { text: 'var(--text)', border: 'var(--border)', bg: 'var(--bg2)' },
6
+ accent: { text: 'var(--violet)', border: 'var(--violet-dim)', bg: 'rgba(167, 139, 250, 0.08)' },
7
+ success: { text: 'var(--green)', border: '#255b3f', bg: 'rgba(34, 197, 94, 0.08)' },
8
+ warning: { text: 'var(--yellow)', border: '#7c5a1a', bg: 'rgba(245, 158, 11, 0.08)' },
9
+ danger: { text: 'var(--red)', border: '#7f1d1d', bg: 'rgba(239, 68, 68, 0.08)' },
10
+ info: { text: 'var(--info)', border: 'rgba(99, 102, 241, 0.28)', bg: 'rgba(99, 102, 241, 0.12)' },
11
+ };
12
+
13
+ export function toneFromHealth(health?: 'ok' | 'warning' | 'error' | 'info'): Tone {
14
+ if (health === 'ok' || health === 'info') return 'success';
15
+ if (health === 'warning') return 'warning';
16
+ if (health === 'error') return 'danger';
17
+ return 'neutral';
18
+ }
@@ -0,0 +1,293 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useCallback } from 'react';
4
+ import { Button, Input, Page, PageHeader, Surface, Pill, Modal } from '../components/ui';
5
+ import PasswordInput from '../components/PasswordInput';
6
+
7
+ interface EnvVars {
8
+ [key: string]: string;
9
+ }
10
+
11
+ const ENV_METADATA: Record<
12
+ string,
13
+ { label: string; description: string; placeholder: string; sensitive?: boolean }
14
+ > = {
15
+ REV4A_PASSWORD: {
16
+ label: 'Login password',
17
+ description: 'Password used to access the Rev4a dashboard',
18
+ placeholder: 'Dashboard password',
19
+ sensitive: true,
20
+ },
21
+ REV4A_TOKEN: {
22
+ label: 'Internal Auth Token',
23
+ description: 'Token for internal API authentication (skills API, cost-override, JWT fallback)',
24
+ placeholder: 'Internal auth token',
25
+ sensitive: true,
26
+ },
27
+ REV4A_API_KEY: {
28
+ label: 'Rev4a API Key',
29
+ description: 'Master key for provider gateway — agents use this to authenticate when calling Rev4a as an LLM provider. Synced to Gateway.',
30
+ placeholder: 'Gateway master key',
31
+ sensitive: true,
32
+ },
33
+ REV4A_JWT_SECRET: {
34
+ label: 'JWT Secret',
35
+ description: 'Secret used to sign session JWT cookies for dashboard authentication',
36
+ placeholder: 'At least 32 characters',
37
+ sensitive: true,
38
+ },
39
+ REV4A_WORKSPACE_ROOT: {
40
+ label: 'Workspace root',
41
+ description: 'Filesystem path for the local file explorer (e.g. /home/user/projects/rev4a). Leave empty to disable the Local workspace.',
42
+ placeholder: '/path/to/workspace',
43
+ sensitive: false,
44
+ },
45
+ };
46
+
47
+ type SaveStatus = 'idle' | 'saving' | 'ok' | 'error';
48
+
49
+ export default function ConfigPageClient() {
50
+ const [env, setEnv] = useState<EnvVars>({});
51
+ const [loading, setLoading] = useState(true);
52
+ const [status, setStatus] = useState<SaveStatus>('idle');
53
+ const [statusMsg, setStatusMsg] = useState('');
54
+ const [restarting, setRestarting] = useState(false);
55
+ const [confirmAction, setConfirmAction] = useState<'save' | 'restart' | null>(null);
56
+
57
+ const loadEnv = useCallback(async () => {
58
+ setLoading(true);
59
+ try {
60
+ const res = await fetch('/api/config/env');
61
+ const data = await res.json();
62
+ if (data.env) setEnv(data.env);
63
+ } catch {
64
+ // ignore
65
+ } finally {
66
+ setLoading(false);
67
+ }
68
+ }, []);
69
+
70
+ useEffect(() => {
71
+ void loadEnv();
72
+ }, [loadEnv]);
73
+
74
+ const handleChange = (key: string, value: string) => {
75
+ setEnv((prev) => ({ ...prev, [key]: value }));
76
+ };
77
+
78
+ const handleSave = async () => {
79
+ setStatus('saving');
80
+ setStatusMsg('');
81
+ try {
82
+ const res = await fetch('/api/config/env', {
83
+ method: 'PUT',
84
+ headers: { 'Content-Type': 'application/json' },
85
+ body: JSON.stringify({ env }),
86
+ });
87
+ const data = await res.json();
88
+ if (data.status === 'ok') {
89
+ setStatus('ok');
90
+ setStatusMsg('Environment variables saved. Restart Rev4a to apply them.');
91
+ } else {
92
+ setStatus('error');
93
+ setStatusMsg(data.error || 'Error');
94
+ }
95
+ } catch (e: unknown) {
96
+ setStatus('error');
97
+ setStatusMsg(e instanceof Error ? e.message : 'Network error');
98
+ }
99
+ };
100
+
101
+
102
+
103
+ // Sort keys: sensitive first, then alphabetical
104
+ const sortedKeys = Object.entries(ENV_METADATA).sort(([, a], [, b]) => {
105
+ if (a.sensitive && !b.sensitive) return -1;
106
+ if (!a.sensitive && b.sensitive) return 1;
107
+ return a.label.localeCompare(b.label);
108
+ });
109
+
110
+ return (
111
+ <Page maxWidth={900}>
112
+ <PageHeader
113
+ eyebrow=""
114
+ title="Configuration"
115
+ description="Environment variables. Values are visible only to you."
116
+ />
117
+
118
+ {loading ? (
119
+ <div style={{ display: 'grid', gap: 12 }}>
120
+ {[1, 2, 3].map((i) => (
121
+ <Surface key={i}>
122
+ <div
123
+ style={{
124
+ height: 18,
125
+ background: 'rgba(255,255,255,0.04)',
126
+ borderRadius: 0,
127
+ marginBottom: 8,
128
+ width: '40%',
129
+ }}
130
+ />
131
+ <div
132
+ style={{
133
+ height: 40,
134
+ background: 'rgba(255,255,255,0.04)',
135
+ borderRadius: 0,
136
+ }}
137
+ />
138
+ </Surface>
139
+ ))}
140
+ </div>
141
+ ) : (
142
+ <>
143
+ <section style={{ display: 'grid', gap: 16, marginBottom: 20 }}>
144
+ {sortedKeys.map(([key, meta]) => {
145
+ const value = env[key] || '';
146
+ return (
147
+ <Surface key={key} variant="panel">
148
+ <div style={{ padding: '14px 16px', display: 'grid', gap: 8 }}>
149
+ <div
150
+ style={{
151
+ display: 'flex',
152
+ alignItems: 'center',
153
+ gap: 8,
154
+ flexWrap: 'wrap',
155
+ }}
156
+ >
157
+ <label
158
+ style={{
159
+ fontWeight: 600,
160
+ fontSize: 13,
161
+ letterSpacing: '0.02em',
162
+ }}
163
+ >
164
+ {meta.label}
165
+ </label>
166
+ {meta.sensitive && <Pill tone="warning">Secret key</Pill>}
167
+ <code
168
+ style={{
169
+ fontSize: 10,
170
+ color: 'var(--text-dim)',
171
+ background: 'rgba(255,255,255,0.04)',
172
+ padding: '2px 6px',
173
+ borderRadius: 0,
174
+ }}
175
+ >
176
+ {key}
177
+ </code>
178
+ </div>
179
+
180
+ <div style={{ fontSize: 12, color: 'var(--text-dim)', lineHeight: 1.4 }}>
181
+ {meta.description}
182
+ </div>
183
+
184
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
185
+ {meta.sensitive ? (
186
+ <PasswordInput
187
+ value={value}
188
+ onChange={(v) => handleChange(key, v)}
189
+ placeholder={meta.placeholder}
190
+ style={{ flex: 1 }}
191
+ showGenerate
192
+ />
193
+ ) : (
194
+ <div style={{ flex: 1 }}>
195
+ <Input
196
+ value={value}
197
+ onChange={(e) => handleChange(key, e.target.value)}
198
+ placeholder={meta.placeholder}
199
+ />
200
+ </div>
201
+ )}
202
+ </div>
203
+ </div>
204
+ </Surface>
205
+ );
206
+ })}
207
+ </section>
208
+
209
+ <div
210
+ style={{
211
+ display: 'flex',
212
+ gap: 12,
213
+ alignItems: 'center',
214
+ marginBottom: 20,
215
+ }}
216
+ >
217
+ <Button
218
+ variant="primary"
219
+ size="md"
220
+ loading={restarting}
221
+ onClick={() => setConfirmAction('restart')}
222
+ >
223
+ {restarting ? 'Restarting...' : 'Save & Restart'}
224
+ </Button>
225
+ </div>
226
+
227
+ {/* Confirm modal */}
228
+ <Modal
229
+ open={confirmAction !== null}
230
+ onClose={() => setConfirmAction(null)}
231
+ title={confirmAction === 'restart' ? 'Restart Rev4a' : 'Save configuration'}
232
+ >
233
+ <div style={{ display: 'grid', gap: 14 }}>
234
+ <p style={{ fontSize: 13, color: 'var(--text-dim)', lineHeight: 1.5, margin: 0 }}>
235
+ {confirmAction === 'restart'
236
+ ? 'This will save the current environment variables and restart the Rev4a server immediately. The dashboard will be unavailable for a few seconds.'
237
+ : 'Save the current environment variables. Changes will take effect after a restart.'}
238
+ </p>
239
+ <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
240
+ <Button variant="ghost" size="sm" onClick={() => setConfirmAction(null)}>
241
+ Cancel
242
+ </Button>
243
+ <Button
244
+ variant="primary"
245
+ size="sm"
246
+ loading={status === 'saving'}
247
+ onClick={async () => {
248
+ setStatus('saving');
249
+ setStatusMsg('');
250
+ try {
251
+ const saveRes = await fetch('/api/config/env', {
252
+ method: 'PUT',
253
+ headers: { 'Content-Type': 'application/json' },
254
+ body: JSON.stringify({ env }),
255
+ });
256
+ const saveData = await saveRes.json();
257
+ if (saveData.status !== 'ok') {
258
+ setStatus('error');
259
+ setStatusMsg(saveData.error || 'Save error');
260
+ setStatus('idle');
261
+ setConfirmAction(null);
262
+ return;
263
+ }
264
+ setStatus('ok');
265
+ setStatusMsg('Environment variables saved.');
266
+ if (confirmAction === 'restart') {
267
+ setRestarting(true);
268
+ await fetch('/api/config/restart', { method: 'POST' });
269
+ setTimeout(() => {
270
+ setRestarting(false);
271
+ setStatus('idle');
272
+ setStatusMsg('');
273
+ }, 4000);
274
+ }
275
+ setConfirmAction(null);
276
+ } catch {
277
+ setStatus('error');
278
+ setStatusMsg('Network error');
279
+ setStatus('idle');
280
+ setConfirmAction(null);
281
+ }
282
+ }}
283
+ >
284
+ {confirmAction === 'restart' ? 'Save & Restart' : 'Save'}
285
+ </Button>
286
+ </div>
287
+ </div>
288
+ </Modal>
289
+ </>
290
+ )}
291
+ </Page>
292
+ );
293
+ }
@@ -0,0 +1,10 @@
1
+ import { Skeleton, CardRowSkeleton } from '../components/Skeleton';
2
+
3
+ const PageSkeleton = () => (
4
+ <div style={{ padding: '24px 20px', maxWidth: 1080, margin: '0 auto' }}>
5
+ <Skeleton width={120} height={20} accent style={{ marginBottom: 20 }} />
6
+ <CardRowSkeleton count={5} height={48} />
7
+ </div>
8
+ );
9
+
10
+ export default PageSkeleton;
@@ -0,0 +1,7 @@
1
+ import ConfigPageClient from './PageClient';
2
+
3
+ export const dynamic = 'force-dynamic';
4
+
5
+ export default function ConfigPage() {
6
+ return <ConfigPageClient />;
7
+ }
@@ -0,0 +1,129 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { Button, Pill } from '../components/ui';
5
+
6
+ interface ContainerInfo {
7
+ id: string;
8
+ name: string;
9
+ image: string;
10
+ status: string;
11
+ state: string;
12
+ ports: string;
13
+ ip: string | null;
14
+ agentId: string | null;
15
+ displayName?: string | null;
16
+ }
17
+
18
+ export default function ContainersClient() {
19
+ const [containers, setContainers] = useState<ContainerInfo[]>([]);
20
+ const [loading, setLoading] = useState(true);
21
+ const [error, setError] = useState('');
22
+
23
+ async function load(signal?: AbortSignal) {
24
+ try {
25
+ const res = await fetch('/api/containers', { signal });
26
+ if (!res.ok) throw new Error(`Error ${res.status}`);
27
+ const data = await res.json();
28
+ if (!signal?.aborted) setContainers(Array.isArray(data) ? data : []);
29
+ } catch(e: any) {
30
+ if (e?.name === 'AbortError') return;
31
+ if (!signal?.aborted) setError(e.message);
32
+ } finally {
33
+ if (!signal?.aborted) setLoading(false);
34
+ }
35
+ }
36
+
37
+ useEffect(() => {
38
+ const controller = new AbortController();
39
+ load(controller.signal);
40
+ const i = setInterval(() => { if (!controller.signal.aborted) load(controller.signal); }, 15000);
41
+ return () => { controller.abort(); clearInterval(i); };
42
+ }, []);
43
+
44
+ return (
45
+ <div style={{ padding: '16px', maxWidth: 1080, margin: '0 auto', height: '100%', overflowY: 'auto', WebkitOverflowScrolling: 'touch', boxSizing: 'border-box', paddingBottom: 72 }}>
46
+ {/* Header */}
47
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16, flexWrap: 'wrap', gap: 8 }}>
48
+ <h1 style={{ fontSize: 20, fontWeight: 400, color: 'var(--violet)', letterSpacing: 2, margin: 0 }}>CONTAINERS</h1>
49
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
50
+ <span style={{ fontSize: 12, color: 'var(--text-dim)' }}>
51
+ {containers.length} container · <span style={{ fontFamily: 'var(--font-mono-stack)', fontSize: 11 }}>{new Date().toLocaleTimeString('it-IT')}</span>
52
+ </span>
53
+ <Button variant="ghost" size="sm" onClick={() => load()}>↻</Button>
54
+ </div>
55
+ </div>
56
+
57
+ {loading && <p style={{ color: 'var(--text-dim)', fontSize: 12, padding: '20px 0' }}>Loading...</p>}
58
+ {error && <p style={{ color: '#ef4444', fontSize: 12 }}>{error}</p>}
59
+
60
+ {/* Container cards */}
61
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
62
+ {containers.map(c => (
63
+ <div
64
+ key={c.id}
65
+ style={{
66
+ padding: '12px 14px',
67
+ border: '1px solid var(--border)',
68
+ borderRadius: 0,
69
+ background: 'var(--bg)',
70
+ display: 'flex',
71
+ flexDirection: 'column',
72
+ gap: 6,
73
+ }}
74
+ >
75
+ {/* Top row: name + status bullet + actions */}
76
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
77
+ <span style={{
78
+ width: 8, height: 8, borderRadius: '50%', flexShrink: 0,
79
+ background: c.state === 'running' ? 'var(--green)' : (c.state === 'restarting' ? 'var(--yellow)' : 'var(--red)')
80
+ }} />
81
+ <span style={{ fontSize: 13, fontWeight: 500, fontFamily: 'var(--font-mono-stack)', wordBreak: 'break-all', flex: '1 1 auto', minWidth: 0 }}>
82
+ {c.name}
83
+ </span>
84
+ <span style={{ fontSize: 11, color: 'var(--text-dim)', flexShrink: 0, fontFamily: 'var(--font-mono-stack)' }}>
85
+ {c.state === 'running' ? c.status.replace(/^Up\s+/, '') : c.status}
86
+ </span>
87
+ {c.state === 'running' && (
88
+ <a
89
+ href={`/containers/terminal/${encodeURIComponent(c.name)}`}
90
+ style={{
91
+ fontSize: 10,
92
+ color: 'var(--violet)',
93
+ textDecoration: 'none',
94
+ border: '1px solid var(--violet)',
95
+ padding: '2px 8px',
96
+ borderRadius: 0,
97
+ flexShrink: 0,
98
+ fontFamily: 'var(--font-mono-stack)',
99
+ letterSpacing: '0.05em',
100
+ }}
101
+ >
102
+ TERMINAL
103
+ </a>
104
+ )}
105
+ </div>
106
+
107
+ {/* Second row: image + ports + ip + agent info */}
108
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', fontSize: 11, color: 'var(--text-dim)' }}>
109
+ <span style={{ fontFamily: 'var(--font-mono-stack)', wordBreak: 'break-all' }}>{c.image}</span>
110
+ {c.ip && <span style={{ fontFamily: 'var(--font-mono-stack)', color: '#888' }}>{c.ip}</span>}
111
+ {c.ports && <span style={{ fontFamily: 'var(--font-mono-stack)', color: '#888' }}>{c.ports}</span>}
112
+ </div>
113
+
114
+ {/* Agent badge — only for Rev4a-managed agents */}
115
+ {(c.displayName || c.agentId) && (
116
+ <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
117
+ {c.displayName ? (
118
+ <Pill tone="accent">{c.displayName}</Pill>
119
+ ) : c.agentId ? (
120
+ <Pill tone="neutral">{c.agentId}</Pill>
121
+ ) : null}
122
+ </div>
123
+ )}
124
+ </div>
125
+ ))}
126
+ </div>
127
+ </div>
128
+ );
129
+ }
@@ -0,0 +1,21 @@
1
+ import { Skeleton, CardRowSkeleton } from '../components/Skeleton';
2
+
3
+ export default function ContainersLoading() {
4
+ return (
5
+ <div style={{
6
+ padding: '16px 20px',
7
+ maxWidth: 1080,
8
+ margin: '0 auto',
9
+ height: '100%',
10
+ boxSizing: 'border-box',
11
+ overflowY: 'auto',
12
+ WebkitOverflowScrolling: 'touch',
13
+ paddingBottom: 72,
14
+ }}>
15
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
16
+ <Skeleton width={140} height={20} accent />
17
+ </div>
18
+ <CardRowSkeleton count={4} height={56} />
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,5 @@
1
+ import ContainersClient from './ContainersClient';
2
+
3
+ export default function ContainersPage() {
4
+ return <ContainersClient />;
5
+ }