@karmaniverous/jeeves-server 3.0.0-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 (260) hide show
  1. package/.env.local +13 -0
  2. package/.env.local.template +13 -0
  3. package/.tsbuildinfo +1 -0
  4. package/CHANGELOG.md +450 -0
  5. package/about.md +82 -0
  6. package/client/README.md +73 -0
  7. package/client/eslint.config.js +23 -0
  8. package/client/index.html +14 -0
  9. package/client/package-lock.json +5181 -0
  10. package/client/package.json +60 -0
  11. package/client/public/vite.svg +1 -0
  12. package/client/src/App.tsx +22 -0
  13. package/client/src/components/AccountMenu.tsx +167 -0
  14. package/client/src/components/ActionDropdown.tsx +120 -0
  15. package/client/src/components/CodeEditor.tsx +143 -0
  16. package/client/src/components/CodeViewer.tsx +113 -0
  17. package/client/src/components/ConfirmDialog.tsx +32 -0
  18. package/client/src/components/DirectoryRow.tsx +62 -0
  19. package/client/src/components/DirectoryTable.tsx +42 -0
  20. package/client/src/components/DownloadDropdown.tsx +116 -0
  21. package/client/src/components/DriveList.tsx +54 -0
  22. package/client/src/components/EmbeddedDiagramPanzoom.ts +28 -0
  23. package/client/src/components/FileContentView.tsx +155 -0
  24. package/client/src/components/InlineSvgPanzoom.ts +60 -0
  25. package/client/src/components/LazyDiagram.ts +93 -0
  26. package/client/src/components/LinkDropdown.tsx +134 -0
  27. package/client/src/components/MarkdownView.tsx +115 -0
  28. package/client/src/components/MermaidViewer.tsx +21 -0
  29. package/client/src/components/PlantUmlViewer.tsx +21 -0
  30. package/client/src/components/SearchModal.tsx +424 -0
  31. package/client/src/components/SvgViewer.tsx +107 -0
  32. package/client/src/components/TabBar.tsx +96 -0
  33. package/client/src/components/layout/Header.tsx +270 -0
  34. package/client/src/components/panzoom.ts +203 -0
  35. package/client/src/components/renderableUtils.ts +15 -0
  36. package/client/src/components/runner/JobTable.tsx +153 -0
  37. package/client/src/components/runner/RunHistory.tsx +140 -0
  38. package/client/src/components/runner/StatsBar.tsx +43 -0
  39. package/client/src/components/runner/StatusPill.tsx +27 -0
  40. package/client/src/components/runner/jobTableUtils.ts +65 -0
  41. package/client/src/components/scrollUtils.ts +39 -0
  42. package/client/src/components/ui/alert-dialog.tsx +107 -0
  43. package/client/src/components/ui/button.tsx +40 -0
  44. package/client/src/components/ui/dropdown-menu.tsx +79 -0
  45. package/client/src/components/ui/input.tsx +26 -0
  46. package/client/src/components/useActionState.ts +43 -0
  47. package/client/src/hooks/useFileBrowser.ts +102 -0
  48. package/client/src/hooks/useFileData.ts +78 -0
  49. package/client/src/hooks/useScrollAnchor.ts +70 -0
  50. package/client/src/hooks/useShareSettings.ts +22 -0
  51. package/client/src/hooks/useTopBar.ts +27 -0
  52. package/client/src/index.css +281 -0
  53. package/client/src/lib/AuthContext.ts +27 -0
  54. package/client/src/lib/api.ts +239 -0
  55. package/client/src/lib/auth.tsx +50 -0
  56. package/client/src/lib/codeBlockCm6.ts +129 -0
  57. package/client/src/lib/codeBlockCopy.ts +43 -0
  58. package/client/src/lib/codemirror.ts +77 -0
  59. package/client/src/lib/runner-api.ts +172 -0
  60. package/client/src/lib/svg.ts +50 -0
  61. package/client/src/lib/theme.ts +34 -0
  62. package/client/src/lib/utils.ts +6 -0
  63. package/client/src/main.tsx +11 -0
  64. package/client/src/pages/FileBrowser.tsx +135 -0
  65. package/client/src/pages/Home.tsx +46 -0
  66. package/client/src/pages/Runner.tsx +151 -0
  67. package/client/src/pages/RunnerJob.tsx +170 -0
  68. package/client/tsconfig.app.json +32 -0
  69. package/client/tsconfig.json +7 -0
  70. package/client/tsconfig.node.json +26 -0
  71. package/client/vite.config.ts +35 -0
  72. package/content/privacy.md +61 -0
  73. package/content/terms.md +41 -0
  74. package/dist/client/assets/CodeEditor-0XHVI8Nu.js +1 -0
  75. package/dist/client/assets/CodeViewer-CykMVsfX.js +1 -0
  76. package/dist/client/assets/index--MBieNJA.js +1 -0
  77. package/dist/client/assets/index-BENeXQI_.js +1 -0
  78. package/dist/client/assets/index-BbBpoOxz.js +1 -0
  79. package/dist/client/assets/index-BdV9g5AM.js +6 -0
  80. package/dist/client/assets/index-BjAilRri.js +2 -0
  81. package/dist/client/assets/index-BqbhWo2I.js +3 -0
  82. package/dist/client/assets/index-CVbycZ0H.js +1 -0
  83. package/dist/client/assets/index-Cs5oz2oJ.js +5 -0
  84. package/dist/client/assets/index-D8KZVveX.js +1 -0
  85. package/dist/client/assets/index-DC4HMHxY.js +13 -0
  86. package/dist/client/assets/index-DbMebkkd.css +1 -0
  87. package/dist/client/assets/index-DcY2RXqX.js +1 -0
  88. package/dist/client/assets/index-Duy-tZYV.js +1 -0
  89. package/dist/client/assets/index-Dw7rDFmE.js +7 -0
  90. package/dist/client/assets/index-FlCUvrjv.js +2 -0
  91. package/dist/client/assets/index-K6OVmfhg.js +1 -0
  92. package/dist/client/assets/index-LjwgzZ7F.js +62 -0
  93. package/dist/client/assets/index-MLwyFRN0.js +1 -0
  94. package/dist/client/assets/index-OpqBpSjn.js +1 -0
  95. package/dist/client/assets/index-SsHei0HE.js +1 -0
  96. package/dist/client/assets/index-uQa2yckk.js +1 -0
  97. package/dist/client/assets/index-udkXoIER.js +1 -0
  98. package/dist/client/index.html +15 -0
  99. package/dist/client/vite.svg +1 -0
  100. package/dist/src/auth/google.js +57 -0
  101. package/dist/src/auth/keys.js +185 -0
  102. package/dist/src/auth/resolve.js +102 -0
  103. package/dist/src/auth/session.js +57 -0
  104. package/dist/src/cli/commands/config.js +100 -0
  105. package/dist/src/cli/commands/config.test.js +84 -0
  106. package/dist/src/cli/commands/service.js +93 -0
  107. package/dist/src/cli/commands/start.js +24 -0
  108. package/dist/src/cli/index.js +20 -0
  109. package/dist/src/config/index.js +90 -0
  110. package/dist/src/config/loadConfig.test.js +127 -0
  111. package/dist/src/config/resolve.js +134 -0
  112. package/dist/src/config/resolve.test.js +148 -0
  113. package/dist/src/config/schema.js +159 -0
  114. package/dist/src/config/substituteEnvVars.js +45 -0
  115. package/dist/src/config/substituteEnvVars.test.js +51 -0
  116. package/dist/src/config/types.js +5 -0
  117. package/dist/src/routes/api/auth-status.js +56 -0
  118. package/dist/src/routes/api/diagrams.js +35 -0
  119. package/dist/src/routes/api/directory.js +93 -0
  120. package/dist/src/routes/api/drives.js +15 -0
  121. package/dist/src/routes/api/export.js +218 -0
  122. package/dist/src/routes/api/fileContent.js +286 -0
  123. package/dist/src/routes/api/index.js +33 -0
  124. package/dist/src/routes/api/linkInfo.js +71 -0
  125. package/dist/src/routes/api/linkInfo.test.js +104 -0
  126. package/dist/src/routes/api/middleware.js +117 -0
  127. package/dist/src/routes/api/raw.js +38 -0
  128. package/dist/src/routes/api/runner.js +59 -0
  129. package/dist/src/routes/api/search.js +236 -0
  130. package/dist/src/routes/api/sharing.js +203 -0
  131. package/dist/src/routes/api/status.js +68 -0
  132. package/dist/src/routes/api/status.test.js +62 -0
  133. package/dist/src/routes/auth.js +99 -0
  134. package/dist/src/routes/event.js +77 -0
  135. package/dist/src/routes/event.test.js +206 -0
  136. package/dist/src/routes/health.js +10 -0
  137. package/dist/src/routes/keys.js +129 -0
  138. package/dist/src/routes/path/index.js +17 -0
  139. package/dist/src/routes/static.js +30 -0
  140. package/dist/src/server.js +90 -0
  141. package/dist/src/services/deepShareLinks.js +163 -0
  142. package/dist/src/services/diagramCache.js +104 -0
  143. package/dist/src/services/embeddedDiagrams.js +136 -0
  144. package/dist/src/services/eventLog.js +55 -0
  145. package/dist/src/services/eventLog.test.js +113 -0
  146. package/dist/src/services/eventQueue.js +154 -0
  147. package/dist/src/services/eventQueue.test.js +104 -0
  148. package/dist/src/services/export.js +220 -0
  149. package/dist/src/services/exportCache.js +196 -0
  150. package/dist/src/services/markdown.js +147 -0
  151. package/dist/src/services/mermaid.js +97 -0
  152. package/dist/src/services/plantuml.js +145 -0
  153. package/dist/src/services/puppeteer.js +156 -0
  154. package/dist/src/util/breadcrumbs.js +22 -0
  155. package/dist/src/util/crypto.js +56 -0
  156. package/dist/src/util/crypto.test.js +99 -0
  157. package/dist/src/util/fileDetection.js +66 -0
  158. package/dist/src/util/fileDetection.test.js +89 -0
  159. package/dist/src/util/formatters.js +43 -0
  160. package/dist/src/util/formatters.test.js +83 -0
  161. package/dist/src/util/packageVersion.js +25 -0
  162. package/dist/src/util/platform.js +148 -0
  163. package/dist/src/util/state.js +46 -0
  164. package/dist/vitest.config.js +12 -0
  165. package/favicon.svg +3 -0
  166. package/guides/access-decision-flow.mmd +24 -0
  167. package/guides/access-decision-flow.svg +1 -0
  168. package/guides/api-integration.md +236 -0
  169. package/guides/deployment.md +287 -0
  170. package/guides/event-gateway.md +204 -0
  171. package/guides/event-gateway.mmd +17 -0
  172. package/guides/event-gateway.svg +1 -0
  173. package/guides/exports.md +239 -0
  174. package/guides/setup.md +313 -0
  175. package/guides/sharing.md +204 -0
  176. package/jeeves-server.config.template.json +25 -0
  177. package/package.json +124 -0
  178. package/scripts/download-plantuml.js +70 -0
  179. package/src/auth/google.ts +93 -0
  180. package/src/auth/keys.ts +252 -0
  181. package/src/auth/resolve.ts +157 -0
  182. package/src/auth/session.ts +77 -0
  183. package/src/cli/commands/config.test.ts +107 -0
  184. package/src/cli/commands/config.ts +113 -0
  185. package/src/cli/commands/service.ts +129 -0
  186. package/src/cli/commands/start.ts +27 -0
  187. package/src/cli/index.ts +25 -0
  188. package/src/config/index.ts +113 -0
  189. package/src/config/loadConfig.test.ts +155 -0
  190. package/src/config/resolve.test.ts +192 -0
  191. package/src/config/resolve.ts +173 -0
  192. package/src/config/schema.ts +179 -0
  193. package/src/config/substituteEnvVars.test.ts +64 -0
  194. package/src/config/substituteEnvVars.ts +52 -0
  195. package/src/config/types.ts +129 -0
  196. package/src/routes/api/auth-status.ts +85 -0
  197. package/src/routes/api/diagrams.ts +53 -0
  198. package/src/routes/api/directory.ts +123 -0
  199. package/src/routes/api/drives.ts +23 -0
  200. package/src/routes/api/export.ts +314 -0
  201. package/src/routes/api/fileContent.ts +414 -0
  202. package/src/routes/api/index.ts +37 -0
  203. package/src/routes/api/linkInfo.test.ts +132 -0
  204. package/src/routes/api/linkInfo.ts +83 -0
  205. package/src/routes/api/middleware.ts +156 -0
  206. package/src/routes/api/raw.ts +54 -0
  207. package/src/routes/api/runner.ts +107 -0
  208. package/src/routes/api/search.ts +321 -0
  209. package/src/routes/api/sharing.ts +259 -0
  210. package/src/routes/api/status.test.ts +72 -0
  211. package/src/routes/api/status.ts +82 -0
  212. package/src/routes/auth.ts +143 -0
  213. package/src/routes/event.test.ts +248 -0
  214. package/src/routes/event.ts +109 -0
  215. package/src/routes/health.ts +13 -0
  216. package/src/routes/keys.ts +192 -0
  217. package/src/routes/path/index.ts +24 -0
  218. package/src/routes/static.ts +54 -0
  219. package/src/server.ts +104 -0
  220. package/src/services/deepShareLinks.ts +203 -0
  221. package/src/services/diagramCache.ts +128 -0
  222. package/src/services/embeddedDiagrams.ts +168 -0
  223. package/src/services/eventLog.test.ts +144 -0
  224. package/src/services/eventLog.ts +68 -0
  225. package/src/services/eventQueue.test.ts +127 -0
  226. package/src/services/eventQueue.ts +196 -0
  227. package/src/services/export.ts +267 -0
  228. package/src/services/exportCache.ts +216 -0
  229. package/src/services/markdown.ts +189 -0
  230. package/src/services/mermaid.ts +113 -0
  231. package/src/services/plantuml.ts +172 -0
  232. package/src/services/puppeteer.ts +188 -0
  233. package/src/types/fastify.d.ts +13 -0
  234. package/src/types/jsonmap.d.ts +10 -0
  235. package/src/types/plantuml-encoder.d.ts +4 -0
  236. package/src/util/breadcrumbs.ts +33 -0
  237. package/src/util/crypto.test.ts +132 -0
  238. package/src/util/crypto.ts +79 -0
  239. package/src/util/fileDetection.test.ts +115 -0
  240. package/src/util/fileDetection.ts +70 -0
  241. package/src/util/formatters.test.ts +105 -0
  242. package/src/util/formatters.ts +44 -0
  243. package/src/util/packageVersion.ts +30 -0
  244. package/src/util/platform.ts +178 -0
  245. package/src/util/state.ts +55 -0
  246. package/test-docs/diagram-retry-test.md +18 -0
  247. package/test-docs/embedded-diagrams.md +52 -0
  248. package/test-docs/lazy-diagrams-test.md +333 -0
  249. package/test-docs/page-a.md +7 -0
  250. package/test-docs/page-b.md +7 -0
  251. package/test-docs/page-c.md +7 -0
  252. package/test-docs/sub/page-d.md +7 -0
  253. package/test-docs/test-diagram.puml +13 -0
  254. package/test-docs/validate-deep-share.js +318 -0
  255. package/tsconfig.json +37 -0
  256. package/tsdoc.json +13 -0
  257. package/vendor/.plantuml-version +1 -0
  258. package/vendor/plantuml.jar +0 -0
  259. package/vitest.config.js +12 -0
  260. package/vitest.config.ts +13 -0
@@ -0,0 +1,50 @@
1
+ /**
2
+ * SVG normalization utilities shared between components.
3
+ */
4
+
5
+ /**
6
+ * Normalize an SVG string for responsive display:
7
+ * - Fix preserveAspectRatio="none" (PlantUML quirk)
8
+ * - Remove inline width/height styles
9
+ * - Set width="100%" and let viewBox handle sizing
10
+ * - Ensure viewBox exists if dimensions are available
11
+ */
12
+ export function normalizeSvg(svgText: string): string {
13
+ const parser = new DOMParser();
14
+ const doc = parser.parseFromString(svgText, 'image/svg+xml');
15
+ const svg = doc.querySelector('svg');
16
+ if (!svg) return svgText;
17
+
18
+ // Extract intrinsic dimensions for viewBox if missing
19
+ let w = 0, h = 0;
20
+ const viewBox = svg.getAttribute('viewBox');
21
+ if (viewBox) {
22
+ const parts = viewBox.split(/[\s,]+/).map(Number);
23
+ if (parts.length === 4) { w = parts[2]; h = parts[3]; }
24
+ }
25
+ if (w <= 0 || h <= 0) {
26
+ w = parseFloat(svg.getAttribute('width') ?? '0');
27
+ h = parseFloat(svg.getAttribute('height') ?? '0');
28
+ }
29
+
30
+ // Ensure viewBox exists
31
+ if (!svg.getAttribute('viewBox') && w > 0 && h > 0) {
32
+ svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
33
+ }
34
+
35
+ // Fix PlantUML preserveAspectRatio="none"
36
+ if (svg.getAttribute('preserveAspectRatio') === 'none') {
37
+ svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
38
+ }
39
+
40
+ // Strip fixed sizing
41
+ svg.removeAttribute('height');
42
+ svg.style.removeProperty('width');
43
+ svg.style.removeProperty('height');
44
+ svg.style.removeProperty('background');
45
+
46
+ // Set responsive sizing
47
+ svg.setAttribute('width', '100%');
48
+
49
+ return new XMLSerializer().serializeToString(doc);
50
+ }
@@ -0,0 +1,34 @@
1
+ import { useState, useCallback, useEffect } from 'react';
2
+
3
+ type Theme = 'light' | 'dark';
4
+
5
+ function applyThemeClass(theme: Theme) {
6
+ if (theme === 'dark') {
7
+ document.documentElement.classList.add('dark');
8
+ } else {
9
+ document.documentElement.classList.remove('dark');
10
+ }
11
+ }
12
+
13
+ export function useTheme(): [Theme, () => void] {
14
+ const [theme, setTheme] = useState<Theme>(() => {
15
+ const saved = localStorage.getItem('jeeves-theme');
16
+ const t = (saved === 'dark' ? 'dark' : 'light') as Theme;
17
+ applyThemeClass(t);
18
+ return t;
19
+ });
20
+
21
+ useEffect(() => {
22
+ applyThemeClass(theme);
23
+ }, [theme]);
24
+
25
+ const toggleTheme = useCallback(() => {
26
+ setTheme((prev) => {
27
+ const next = prev === 'dark' ? 'light' : 'dark';
28
+ localStorage.setItem('jeeves-theme', next);
29
+ return next;
30
+ });
31
+ }, []);
32
+
33
+ return [theme, toggleTheme];
34
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,11 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+
4
+ import App from './App.tsx';
5
+ import './index.css';
6
+
7
+ createRoot(document.getElementById('root')!).render(
8
+ <StrictMode>
9
+ <App />
10
+ </StrictMode>,
11
+ );
@@ -0,0 +1,135 @@
1
+ /**
2
+ * FileBrowser — thin composition root that wires hooks and components together.
3
+ */
4
+ import { ConfirmDialog } from '@/components/ConfirmDialog';
5
+ import { DirectoryTable } from '@/components/DirectoryTable';
6
+ import { DownloadDropdown } from '@/components/DownloadDropdown';
7
+ import { DriveList } from '@/components/DriveList';
8
+ import { FileContentView } from '@/components/FileContentView';
9
+ import { Header } from '@/components/layout/Header';
10
+ import { LinkDropdown } from '@/components/LinkDropdown';
11
+ import { TabBar } from '@/components/TabBar';
12
+ import { useFileBrowser } from '@/hooks/useFileBrowser';
13
+
14
+ export function FileBrowser() {
15
+ const {
16
+ reqPath, theme, toggleTheme,
17
+ shareSettings, setShareSettings,
18
+ mobileTocOpen, setMobileTocOpen,
19
+ proseWidth, toggleProseWidth,
20
+ drives, directory, fileRaw, fileRendered, file,
21
+ loading, error, editing, setEditing,
22
+ viewTab, setViewTab,
23
+ breadcrumbs, isInsider, searchEnabled, keyAge,
24
+ rotateKeyDialogOpen, setRotateKeyDialogOpen,
25
+ handleRotateKey, confirmRotateKey,
26
+ topBarRef, mainRef, topBarHeight,
27
+ handleSave,
28
+ } = useFileBrowser();
29
+
30
+ const showFileView = file || (loading && !!reqPath);
31
+
32
+ return (
33
+ <div className="min-h-screen bg-background text-foreground">
34
+ {/* Fixed top bar */}
35
+ <div ref={topBarRef} className="fixed top-0 left-0 right-0 z-50">
36
+ <Header
37
+ breadcrumbs={breadcrumbs}
38
+ isInsider={isInsider}
39
+ searchEnabled={searchEnabled}
40
+ theme={theme}
41
+ onToggleTheme={toggleTheme}
42
+ keyAge={editing ? undefined : keyAge}
43
+ onRotateKey={editing ? undefined : handleRotateKey}
44
+ downloadDropdown={editing ? undefined :
45
+ file ? (
46
+ <DownloadDropdown reqPath={reqPath} file={file} isInsider={isInsider} variant="header" />
47
+ ) : directory ? (
48
+ <DownloadDropdown reqPath={reqPath} file={null} isDirectory isInsider={isInsider} variant="header" />
49
+ ) : undefined
50
+ }
51
+ downloadMenuItem={editing ? undefined :
52
+ file ? (
53
+ (onDismiss) => <DownloadDropdown reqPath={reqPath} file={file} isInsider={isInsider} variant="menuItem" onStateChange={(s) => { if (s === 'done') setTimeout(onDismiss, 800); }} />
54
+ ) : directory ? (
55
+ (onDismiss) => <DownloadDropdown reqPath={reqPath} file={null} isDirectory isInsider={isInsider} variant="menuItem" onStateChange={(s) => { if (s === 'done') setTimeout(onDismiss, 800); }} />
56
+ ) : undefined
57
+ }
58
+ linkControls={editing ? undefined : isInsider ? (
59
+ <LinkDropdown path={`/${reqPath}`} shareSettings={shareSettings} onShareSettingsChange={setShareSettings} showEvent showRaw={!!file} variant="header" isDirectory={!file} />
60
+ ) : undefined}
61
+ linkMenuItem={editing ? undefined : isInsider ? (
62
+ (onDismiss) => <LinkDropdown path={`/${reqPath}`} shareSettings={shareSettings} onShareSettingsChange={setShareSettings} showEvent showRaw={!!file} variant="menuItem" isDirectory={!file} onStateChange={(s) => { if (s === 'done') setTimeout(onDismiss, 800); }} />
63
+ ) : undefined}
64
+ />
65
+
66
+ {/* Tabs for file views */}
67
+ {showFileView && (
68
+ <TabBar
69
+ reqPath={reqPath}
70
+ file={file}
71
+ fileRendered={fileRendered}
72
+ viewTab={viewTab}
73
+ setViewTab={setViewTab}
74
+ proseWidth={proseWidth}
75
+ toggleProseWidth={toggleProseWidth}
76
+ isInsider={isInsider}
77
+ editing={editing}
78
+ setEditing={setEditing}
79
+ mobileTocOpen={mobileTocOpen}
80
+ setMobileTocOpen={setMobileTocOpen}
81
+ loading={loading}
82
+ />
83
+ )}
84
+ </div>
85
+
86
+ <main
87
+ ref={mainRef}
88
+ className={showFileView ? 'px-0 pb-32 overflow-y-auto' : 'p-4 pb-32 md:px-6 md:pt-6 overflow-y-auto'}
89
+ style={{ marginTop: `${topBarHeight}px`, height: `calc(100vh - ${topBarHeight}px)` }}
90
+ >
91
+ {loading && !reqPath && <div className="text-muted-foreground text-sm">Loading...</div>}
92
+ {error && <div className="text-destructive text-sm">Error: {error}</div>}
93
+
94
+ {/* Drive listing */}
95
+ {!loading && !error && drives && (
96
+ <DriveList drives={drives} isInsider={isInsider} shareSettings={shareSettings} onShareSettingsChange={setShareSettings} />
97
+ )}
98
+
99
+ {/* Directory listing */}
100
+ {!loading && !error && directory && (
101
+ <DirectoryTable entries={directory.entries} basePath={reqPath} isInsider={isInsider} shareSettings={shareSettings} onShareSettingsChange={setShareSettings} />
102
+ )}
103
+
104
+ {/* File viewer */}
105
+ {!error && showFileView && (
106
+ <FileContentView
107
+ reqPath={reqPath}
108
+ file={file}
109
+ fileRaw={fileRaw}
110
+ fileRendered={fileRendered}
111
+ viewTab={viewTab}
112
+ editing={editing}
113
+ setEditing={setEditing}
114
+ proseWidth={proseWidth}
115
+ topBarHeight={topBarHeight}
116
+ mainRef={mainRef}
117
+ mobileTocOpen={mobileTocOpen}
118
+ setMobileTocOpen={setMobileTocOpen}
119
+ onSave={handleSave}
120
+ loading={loading}
121
+ />
122
+ )}
123
+ </main>
124
+
125
+ <ConfirmDialog
126
+ open={rotateKeyDialogOpen}
127
+ onOpenChange={setRotateKeyDialogOpen}
128
+ title="Rotate insider key?"
129
+ description="This will invalidate ALL existing share links generated with your current key. This action cannot be undone."
130
+ confirmLabel="Rotate Key"
131
+ onConfirm={() => void confirmRotateKey()}
132
+ />
133
+ </div>
134
+ );
135
+ }
@@ -0,0 +1,46 @@
1
+ import { useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+
4
+ import { useAuth } from '@/lib/AuthContext';
5
+
6
+ export function Home() {
7
+ const { loading, authenticated } = useAuth();
8
+ const navigate = useNavigate();
9
+ useEffect(() => {
10
+ if (!loading && authenticated) {
11
+ navigate('/browse', { replace: true });
12
+ }
13
+ }, [loading, authenticated, navigate]);
14
+
15
+ if (loading) {
16
+ return (
17
+ <div className="min-h-screen bg-background flex items-center justify-center">
18
+ <div className="text-muted-foreground text-sm">Loading...</div>
19
+ </div>
20
+ );
21
+ }
22
+
23
+ return (
24
+ <div className="min-h-screen bg-background flex items-center justify-center">
25
+ <div className="text-center space-y-6">
26
+ <div className="text-6xl mb-2">🎩</div>
27
+ <h1 className="text-2xl font-semibold text-foreground">Jeeves</h1>
28
+ <p className="text-muted-foreground text-sm max-w-xs mx-auto">
29
+ File browser and document server
30
+ </p>
31
+ <a
32
+ href="/auth/login?returnTo=/browse"
33
+ className="inline-flex items-center gap-3 px-6 py-3 bg-white text-zinc-800 rounded-lg shadow-md hover:shadow-lg transition-shadow font-medium text-sm border border-zinc-200"
34
+ >
35
+ <svg viewBox="0 0 24 24" width="20" height="20" className="shrink-0">
36
+ <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" />
37
+ <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
38
+ <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
39
+ <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
40
+ </svg>
41
+ Sign in with Google
42
+ </a>
43
+ </div>
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Runner dashboard — job list view with stats summary and auto-refresh.
3
+ */
4
+
5
+ import { RefreshCw } from 'lucide-react';
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
+
8
+ import { Header } from '@/components/layout/Header';
9
+ import { JobTableBody, JobTableHeader, } from '@/components/runner/JobTable';
10
+ import { nextSort, sortJobs } from '@/components/runner/jobTableUtils';
11
+ import type { SortColumn, SortState } from '@/components/runner/JobTable';
12
+ import { StatsBar } from '@/components/runner/StatsBar';
13
+ import { Button } from '@/components/ui/button';
14
+ import { useAuth } from '@/lib/AuthContext';
15
+ import type { RunnerJob, RunnerStats } from '@/lib/runner-api';
16
+ import { getRunnerJobs, getRunnerStats, triggerJobRun } from '@/lib/runner-api';
17
+ import { useTheme } from '@/lib/theme';
18
+
19
+ const REFRESH_INTERVAL = 10_000;
20
+
21
+ export function Runner() {
22
+ const [jobs, setJobs] = useState<RunnerJob[]>([]);
23
+ const [stats, setStats] = useState<RunnerStats | null>(null);
24
+ const [error, setError] = useState<string | null>(null);
25
+ const [loading, setLoading] = useState(true);
26
+ const [autoRefresh, setAutoRefresh] = useState(true);
27
+ const [sort, setSort] = useState<SortState>({ column: null, direction: 'desc' });
28
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
29
+ const [theme, toggleTheme] = useTheme();
30
+ const { isInsider, searchEnabled, keyCreatedAt, rotateKey } = useAuth();
31
+
32
+ // Compute key age string
33
+ const keyAge = keyCreatedAt
34
+ ? `${Math.floor((Date.now() - new Date(keyCreatedAt).getTime()) / 86_400_000)}d`
35
+ : null;
36
+
37
+ const fetchData = useCallback(async () => {
38
+ try {
39
+ const [jobsData, statsData] = await Promise.all([
40
+ getRunnerJobs(),
41
+ getRunnerStats(),
42
+ ]);
43
+ setJobs(jobsData);
44
+ setStats(statsData);
45
+ setError(null);
46
+ } catch (err) {
47
+ setError(err instanceof Error ? err.message : 'Failed to fetch runner data');
48
+ } finally {
49
+ setLoading(false);
50
+ }
51
+ }, []);
52
+
53
+ useEffect(() => {
54
+ void fetchData();
55
+ }, [fetchData]);
56
+
57
+ useEffect(() => {
58
+ if (autoRefresh) {
59
+ intervalRef.current = setInterval(() => void fetchData(), REFRESH_INTERVAL);
60
+ }
61
+ return () => {
62
+ if (intervalRef.current) clearInterval(intervalRef.current);
63
+ };
64
+ }, [autoRefresh, fetchData]);
65
+
66
+ const handleRunNow = useCallback(async (id: string) => {
67
+ try {
68
+ await triggerJobRun(id);
69
+ await fetchData();
70
+ } catch {
71
+ await fetchData();
72
+ }
73
+ }, [fetchData]);
74
+
75
+ const handleSort = useCallback((column: SortColumn) => {
76
+ setSort((prev) => nextSort(prev, column));
77
+ }, []);
78
+
79
+ const sortedJobs = useMemo(() => sortJobs(jobs, sort), [jobs, sort]);
80
+
81
+ const breadcrumbs = [{ label: 'Runner', path: 'runner' }];
82
+
83
+ return (
84
+ <div className={`h-screen overflow-hidden ${theme === 'dark' ? 'dark' : ''}`}>
85
+ <div className="h-full flex flex-col bg-background text-foreground">
86
+ {/* Shared header */}
87
+ <Header
88
+ breadcrumbs={breadcrumbs}
89
+ isInsider={isInsider}
90
+ searchEnabled={searchEnabled}
91
+ theme={theme}
92
+ onToggleTheme={toggleTheme}
93
+ keyAge={keyAge}
94
+ onRotateKey={rotateKey}
95
+ />
96
+
97
+ {/* Runner controls bar */}
98
+ <div className="shrink-0 max-w-6xl mx-auto w-full px-4 pt-2 pb-1 flex items-center justify-between">
99
+ {!loading && <StatsBar stats={stats} />}
100
+ <div className="flex items-center gap-2 shrink-0">
101
+ <label className="flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer">
102
+ <input
103
+ type="checkbox"
104
+ checked={autoRefresh}
105
+ onChange={(e) => setAutoRefresh(e.target.checked)}
106
+ className="rounded"
107
+ />
108
+ Auto-refresh
109
+ </label>
110
+ <Button
111
+ variant="ghost"
112
+ size="icon"
113
+ className="h-7 w-7"
114
+ title="Refresh now"
115
+ onClick={() => void fetchData()}
116
+ >
117
+ <RefreshCw className="h-3.5 w-3.5" />
118
+ </Button>
119
+ </div>
120
+ </div>
121
+
122
+ {/* Error banner */}
123
+ {error && (
124
+ <div className="shrink-0 max-w-6xl mx-auto w-full px-4 pt-2">
125
+ <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded-lg text-sm">
126
+ {error}
127
+ </div>
128
+ </div>
129
+ )}
130
+
131
+ {loading ? (
132
+ <div className="flex-1 flex items-center justify-center text-muted-foreground">Loading...</div>
133
+ ) : (
134
+ /* Single card: header pinned, body scrolls, scrollbar inside card */
135
+ <div className="flex-1 min-h-0 max-w-6xl mx-auto w-full px-4 pt-1 flex flex-col">
136
+ <div className="flex-1 min-h-0 bg-card border border-border rounded-lg flex flex-col">
137
+ {/* Pinned table header */}
138
+ <div className="shrink-0">
139
+ <JobTableHeader sort={sort} onSort={handleSort} />
140
+ </div>
141
+ {/* Scrollable table body — padding inside the scroll for mobile bottom space */}
142
+ <div className="flex-1 overflow-y-auto pb-32">
143
+ <JobTableBody jobs={sortedJobs} onRunNow={(id) => void handleRunNow(id)} />
144
+ </div>
145
+ </div>
146
+ </div>
147
+ )}
148
+ </div>
149
+ </div>
150
+ );
151
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Runner job detail view — job info, enable/disable, run history.
3
+ */
4
+
5
+ import { ArrowLeft, Play, Power, PowerOff } from 'lucide-react';
6
+ import { useCallback, useEffect, useState } from 'react';
7
+ import { Link, useParams } from 'react-router-dom';
8
+
9
+ import { Header } from '@/components/layout/Header';
10
+ import { RunHistory } from '@/components/runner/RunHistory';
11
+ import { StatusPill } from '@/components/runner/StatusPill';
12
+ import { Button } from '@/components/ui/button';
13
+ import { useAuth } from '@/lib/AuthContext';
14
+ import type { RunEntry, RunnerJob as RunnerJobType } from '@/lib/runner-api';
15
+ import {
16
+ disableJob,
17
+ enableJob,
18
+ getJobRuns,
19
+ getRunnerJob,
20
+ triggerJobRun,
21
+ } from '@/lib/runner-api';
22
+ import { useTheme } from '@/lib/theme';
23
+
24
+ export function RunnerJob() {
25
+ const { jobId } = useParams<{ jobId: string }>();
26
+ const [job, setJob] = useState<RunnerJobType | null>(null);
27
+ const [runs, setRuns] = useState<RunEntry[]>([]);
28
+ const [error, setError] = useState<string | null>(null);
29
+ const [loading, setLoading] = useState(true);
30
+ const [theme, toggleTheme] = useTheme();
31
+ const { isInsider, searchEnabled, keyCreatedAt, rotateKey } = useAuth();
32
+
33
+ const keyAge = keyCreatedAt
34
+ ? `${Math.floor((Date.now() - new Date(keyCreatedAt).getTime()) / 86_400_000)}d`
35
+ : null;
36
+
37
+ const fetchData = useCallback(async () => {
38
+ if (!jobId) return;
39
+ try {
40
+ const [jobData, runsData] = await Promise.all([
41
+ getRunnerJob(jobId),
42
+ getJobRuns(jobId, 50),
43
+ ]);
44
+ setJob(jobData);
45
+ setRuns(runsData);
46
+ setError(null);
47
+ } catch (err) {
48
+ setError(err instanceof Error ? err.message : 'Failed to fetch job');
49
+ } finally {
50
+ setLoading(false);
51
+ }
52
+ }, [jobId]);
53
+
54
+ useEffect(() => {
55
+ void fetchData();
56
+ }, [fetchData]);
57
+
58
+ const handleToggleEnabled = useCallback(async () => {
59
+ if (!job) return;
60
+ try {
61
+ if (job.enabled) await disableJob(job.id);
62
+ else await enableJob(job.id);
63
+ await fetchData();
64
+ } catch {
65
+ await fetchData();
66
+ }
67
+ }, [job, fetchData]);
68
+
69
+ const handleRunNow = useCallback(async () => {
70
+ if (!job) return;
71
+ try {
72
+ await triggerJobRun(job.id);
73
+ setTimeout(() => void fetchData(), 500);
74
+ } catch {
75
+ await fetchData();
76
+ }
77
+ }, [job, fetchData]);
78
+
79
+ const breadcrumbs = [
80
+ { label: 'Runner', path: 'runner' },
81
+ { label: job?.name ?? jobId ?? '', path: `runner/${jobId}` },
82
+ ];
83
+
84
+ return (
85
+ <div className={`h-screen overflow-hidden ${theme === 'dark' ? 'dark' : ''}`}>
86
+ <div className="h-full flex flex-col bg-background text-foreground">
87
+ {/* Shared header */}
88
+ <Header
89
+ breadcrumbs={breadcrumbs}
90
+ isInsider={isInsider}
91
+ searchEnabled={searchEnabled}
92
+ theme={theme}
93
+ onToggleTheme={toggleTheme}
94
+ keyAge={keyAge}
95
+ onRotateKey={rotateKey}
96
+ />
97
+
98
+ <div className="flex-1 overflow-y-auto max-w-4xl mx-auto px-4 py-6 pb-32 space-y-6 w-full">
99
+ <Link
100
+ to="/runner"
101
+ className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
102
+ >
103
+ <ArrowLeft className="h-4 w-4" />
104
+ Back to jobs
105
+ </Link>
106
+
107
+ {error && (
108
+ <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded-lg text-sm">
109
+ {error}
110
+ </div>
111
+ )}
112
+
113
+ {loading ? (
114
+ <div className="text-center py-12 text-muted-foreground">Loading...</div>
115
+ ) : job ? (
116
+ <>
117
+ {/* Job Info Card */}
118
+ <div className="bg-card border border-border rounded-lg p-4 space-y-4">
119
+ <div className="flex items-start justify-between">
120
+ <div>
121
+ <h1 className="text-xl font-semibold">{job.name}</h1>
122
+ <div className="flex items-center gap-3 mt-2 text-sm text-muted-foreground">
123
+ <span>Type: <span className="text-foreground">{job.type}</span></span>
124
+ <span>Schedule: <code className="text-xs bg-muted px-1 py-0.5 rounded">{job.schedule}</code></span>
125
+ <span>Overlap: <span className="text-foreground">{job.overlapPolicy}</span></span>
126
+ </div>
127
+ </div>
128
+ <StatusPill status={job.enabled ? job.status : 'disabled'} />
129
+ </div>
130
+
131
+ <div className="flex items-center gap-2 pt-2 border-t border-border">
132
+ <Button
133
+ variant="outline"
134
+ size="sm"
135
+ onClick={() => void handleToggleEnabled()}
136
+ className="gap-1.5"
137
+ >
138
+ {job.enabled
139
+ ? <><PowerOff className="h-3.5 w-3.5" /> Disable</>
140
+ : <><Power className="h-3.5 w-3.5" /> Enable</>
141
+ }
142
+ </Button>
143
+ <Button
144
+ variant="outline"
145
+ size="sm"
146
+ onClick={() => void handleRunNow()}
147
+ className="gap-1.5"
148
+ >
149
+ <Play className="h-3.5 w-3.5" />
150
+ Run Now
151
+ </Button>
152
+ </div>
153
+ </div>
154
+
155
+ {/* Run History */}
156
+ <div className="bg-card border border-border rounded-lg overflow-hidden">
157
+ <div className="px-4 py-3 border-b border-border">
158
+ <h2 className="text-sm font-medium text-muted-foreground">Run History</h2>
159
+ </div>
160
+ <RunHistory runs={runs} />
161
+ </div>
162
+ </>
163
+ ) : (
164
+ <div className="text-center py-12 text-muted-foreground">Job not found</div>
165
+ )}
166
+ </div>
167
+ </div>
168
+ </div>
169
+ );
170
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "erasableSyntaxOnly": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUncheckedSideEffectImports": true,
26
+ "baseUrl": ".",
27
+ "paths": {
28
+ "@/*": ["./src/*"]
29
+ }
30
+ },
31
+ "include": ["src"]
32
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }