diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Header/Header.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Header/Header.tsx index 3d38434b..9c7dc408 100644 --- a/src/apps/officer-web/Screens/Dashboard/Layout/Header/Header.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Layout/Header/Header.tsx @@ -6,6 +6,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sh import type { DockItem } from '../Dock'; import { useIsTouch } from '../useIsTouch'; import { UserMenu } from './UserMenu'; +import { RescanButton } from '../Rescan/RescanButton'; // import { BugReportButton } from '../BugReport/BugReportButton'; type HeaderProps = { @@ -52,6 +53,7 @@ export function Header({ dockItems }: HeaderProps) { */} {/* Bug Report — hidden */} {/* */} + diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Rescan/RescanButton.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Rescan/RescanButton.tsx new file mode 100644 index 00000000..ff4e79eb --- /dev/null +++ b/src/apps/officer-web/Screens/Dashboard/Layout/Rescan/RescanButton.tsx @@ -0,0 +1,44 @@ +import { useState } from 'react'; +import { RotateCw } from 'lucide-react'; +import { useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; +import { useClient } from 'hooks/useClient'; + +type RescanResponse = { ok: boolean; counts: Record }; + +// Item query caches the header refreshes after a rescan (matches the queryKeys used by the +// Automation and Capability pages). +const ITEM_QUERY_KEYS = ['tasks', 'skills', 'tools', 'processes']; + +export function RescanButton() { + const client = useClient(); + const qc = useQueryClient(); + const [loading, setLoading] = useState(false); + + const rescan = async () => { + if (loading) return; + setLoading(true); + try { + const res = await client.post('/rescan', {}); + await Promise.all(ITEM_QUERY_KEYS.map((key) => qc.invalidateQueries({ queryKey: [key] }))); + const c = res.counts ?? {}; + toast.success(`Rescanned items — ${c.tasks ?? 0} tasks, ${c.tools ?? 0} tools, ${c.skills ?? 0} skills, ${c.processes ?? 0} processes`); + } catch { + toast.error('Failed to rescan items'); + } finally { + setLoading(false); + } + }; + + return ( + + ); +} diff --git a/src/servers/api/items/rescan.ts b/src/servers/api/items/rescan.ts new file mode 100644 index 00000000..1dbfddcd --- /dev/null +++ b/src/servers/api/items/rescan.ts @@ -0,0 +1,39 @@ +import { readdirSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { createRouter } from '../../create-router'; +import { ITEM_TYPES, itemsDir, ensureItemDirs, type ItemType } from '../../data-path'; +import { ensureToolLoader } from '../../ensure-tool-loader'; + +// Re-applies the boot-time item setup so changes under OFFICER_ITEMS_DIR take effect without a +// server restart. Item lists are already read fresh from disk on every request; this ensures the +// type dirs exist and the tool-loader extension is current, and reports the live item counts so the +// UI can refetch. + +const CANONICAL: Record = { + skills: 'SKILL.md', + tools: 'TOOL.md', + tasks: 'TASK.md', + processes: 'PROCESS.md', + extensions: 'index.ts', +}; + +function countItems(type: ItemType): number { + const dir = itemsDir(type); + if (!existsSync(dir)) return 0; + let count = 0; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.isDirectory() && existsSync(join(dir, entry.name, CANONICAL[type]))) count++; + } + return count; +} + +export const rescanRouter = createRouter(); + +rescanRouter.post('/', async (ctx) => { + ensureItemDirs(); + ensureToolLoader(); + + const counts = Object.fromEntries(ITEM_TYPES.map((type) => [type, countItems(type)])) as Record; + + return ctx.json({ ok: true, counts }); +}); diff --git a/src/servers/ensure-tool-loader.ts b/src/servers/ensure-tool-loader.ts index ea07c74b..cb223eb6 100644 --- a/src/servers/ensure-tool-loader.ts +++ b/src/servers/ensure-tool-loader.ts @@ -1,11 +1,11 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; -import { DATA_PATH } from './data-path'; +import { itemsDir } from './data-path'; const SOURCE_PATH = resolve(import.meta.dir, 'tool-loader-source.ts'); export function ensureToolLoader(): void { - const targetDir = join(DATA_PATH, 'extensions', 'tool-loader'); + const targetDir = join(itemsDir('extensions'), 'tool-loader'); const targetFile = join(targetDir, 'index.ts'); mkdirSync(targetDir, { recursive: true }); diff --git a/src/servers/hono.ts b/src/servers/hono.ts index dfc065f6..cfee0dba 100644 --- a/src/servers/hono.ts +++ b/src/servers/hono.ts @@ -12,6 +12,7 @@ import { skillsRouter } from './api/skills/skills'; import { tasksRouter } from './api/tasks/tasks'; import { toolsRouter } from './api/tools/tools'; import { processesRouter } from './api/processes/processes'; +import { rescanRouter } from './api/items/rescan'; import { scrapeRouter } from './api/scrape/scrape'; import { uploadRouter } from './api/upload/upload'; import { settingsRouter } from './api/settings/settings'; @@ -85,6 +86,7 @@ protectedRouter.route('/skills', skillsRouter); protectedRouter.route('/tasks', tasksRouter); protectedRouter.route('/tools', toolsRouter); protectedRouter.route('/processes', processesRouter); +protectedRouter.route('/rescan', rescanRouter); protectedRouter.route('/scrape', scrapeRouter); protectedRouter.route('/upload', uploadRouter); protectedRouter.route('/user', settingsRouter);