"Activity" (placeholder name — jobs/tasks were taken) = watch background tasks
scroll in parallel with chat. Built DB-free; NOT restarted — deploy + test in
the morning.
- NDJSON progress contract (activity/progress.ts): capabilities append
{job,cap,phase,status,pct,detail,ts,...} lines; tolerant parser treats any
JSON object with phase/status as structured progress, else a raw log line.
- Backend (activity/router.ts, owner-only, path-guarded):
- GET /api/activity/tasks — registry by scanning /tmp/claude-*/<cwd>/tasks/
*.output (harness run_in_background) + announced detached jobs.
- POST /api/activity/announce {name,path} — register a detached (setsid) job's
log so it's followable too (the setsid case is on the critical path, since
the warm worker now makes plain run_in_background the default for heavy jobs).
- GET /api/activity/stream?task=<id>|path=<abs> — SSE tail (poll + offset),
emitting {kind:'line'|'progress'} with NDJSON parsed.
- Frontend /activity screen + Radio nav item: task list (active dot) → live tail
with a phase/pct progress header + raw log, following the /system-monitor pattern.
- Retention: startChatEventRetention() prunes chat_session_events >7d every 6h
(wired in bootstrap) so the durable queue stays bounded.
Verified headlessly (no restart): parseTailLine classification, and the scan
finds 53 real task output files. Endpoints + UI untested until deploy.
Deferred (see handoff): cross-device sync + OpenCode parity (both touch the
now-stable chat path — won't ship un-restart-tested); task:progress into chat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
132 lines
5.9 KiB
TypeScript
132 lines
5.9 KiB
TypeScript
import { Hono } from 'hono';
|
|
import { cors } from 'hono/cors';
|
|
import { createRouter } from './create-router';
|
|
import type { HonoVariables } from './create-router';
|
|
import { authRouter } from './api/auth';
|
|
import { serverSettingsRouter } from './api/server-settings/server-settings';
|
|
import { landingPageDataRouter } from './api/landing-page-data/landing-page-data';
|
|
import { waitlistRouter } from './api/waitlist/waitlist';
|
|
import { usersRouter } from './api/users/users-router';
|
|
import { plansRouter } from './api/plans/plans';
|
|
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';
|
|
import { dashboardsRouter } from './api/dashboards';
|
|
import { taskLogsRouter } from './api/task-logs/task-logs';
|
|
import { router as fileBrowserRouter } from './api/file-browser/router';
|
|
import { musicRouter } from './api/music/router';
|
|
import { systemMonitorRouter } from './api/system-monitor/system-monitor';
|
|
import { activityRouter } from './api/activity/router';
|
|
import './api/music/sidecar-server'; // side-effect: capture the officer-music audio server port
|
|
import { devServerRouter, devServerProxyRouter } from './api/dev-server/router';
|
|
import { dockRouter } from './api/dock/dock';
|
|
import { integrationsRouter, googleCallbackHandler } from './api/integrations/integrations';
|
|
import { queueRouter } from './api/queue/queue';
|
|
import { emailRouter } from './api/email/email';
|
|
import { channelsRouter } from './channels/routes';
|
|
import { browserRouter } from './api/browser/router';
|
|
import { desktopRouter } from './api/desktop/rest';
|
|
import { appsRouter, appServeRouter } from './api/apps';
|
|
import { bugReportRouter } from './api/bug-report/bug-report';
|
|
import { chatRouter } from './api/chat/chat';
|
|
import { pipelineJobsRouter } from './api/tasks/pipeline-jobs-routes';
|
|
import { broadcastPanelRefresh } from './api/terminal/websocket';
|
|
import { CustomError } from './custom-errors';
|
|
import { userMiddleware, bodyParser, isOriginAllowed, originScopeMiddleware } from './_middlewares';
|
|
|
|
export { Hono };
|
|
export { createRouter };
|
|
export type { HonoVariables };
|
|
|
|
export const honoServer = new Hono<{ Variables: HonoVariables }>();
|
|
|
|
honoServer.use(
|
|
cors({
|
|
origin: (origin, c) => {
|
|
const host = c.req.header('host');
|
|
return isOriginAllowed(origin, host) ? origin : '';
|
|
},
|
|
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
allowHeaders: ['Content-Type', 'Authorization'],
|
|
}),
|
|
);
|
|
|
|
// Scoped-origin gate: restrict app origins (e.g. the music app) to their allowed path prefixes
|
|
// (/api/auth + /api/music). No-ops for the main web origin and while MUSIC_APP_ORIGIN is unset.
|
|
honoServer.use(originScopeMiddleware);
|
|
|
|
honoServer.get('/api', (ctx) => ctx.json({ officerAPI: 'ok' }));
|
|
honoServer.route('/api/auth', authRouter);
|
|
honoServer.route('/api/landing-page-data', landingPageDataRouter);
|
|
honoServer.route('/api/waitlist', waitlistRouter);
|
|
honoServer.route('/api/dev-server-proxy', devServerProxyRouter);
|
|
honoServer.route('/api/app-serve', appServeRouter);
|
|
honoServer.get('/api/integrations/google/callback', googleCallbackHandler);
|
|
honoServer.post('/api/hooks/claude-done', async (ctx) => {
|
|
const body = await ctx.req.json().catch(() => null);
|
|
const email = (body as Record<string, unknown> | null)?.email;
|
|
if (typeof email === 'string' && email.includes('@')) {
|
|
broadcastPanelRefresh(email);
|
|
}
|
|
return ctx.json({ ok: true });
|
|
});
|
|
|
|
const protectedRouter = createRouter();
|
|
protectedRouter.use(bodyParser());
|
|
protectedRouter.use(userMiddleware);
|
|
|
|
protectedRouter.route('/server-settings', serverSettingsRouter);
|
|
protectedRouter.route('/users', usersRouter);
|
|
protectedRouter.route('/plans', plansRouter);
|
|
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);
|
|
protectedRouter.route('/dashboards', dashboardsRouter);
|
|
protectedRouter.route('/task-logs', taskLogsRouter);
|
|
protectedRouter.route('/file-browser', fileBrowserRouter);
|
|
protectedRouter.route('/music', musicRouter);
|
|
protectedRouter.route('/system-monitor', systemMonitorRouter);
|
|
protectedRouter.route('/activity', activityRouter);
|
|
protectedRouter.route('/dev-server', devServerRouter);
|
|
protectedRouter.route('/dock', dockRouter);
|
|
protectedRouter.route('/integrations', integrationsRouter);
|
|
protectedRouter.route('/queue', queueRouter);
|
|
protectedRouter.route('/email', emailRouter);
|
|
protectedRouter.route('/channels', channelsRouter);
|
|
protectedRouter.route('/browser', browserRouter);
|
|
protectedRouter.route('/apps', appsRouter);
|
|
protectedRouter.route('/bug-report', bugReportRouter);
|
|
protectedRouter.route('/chat', chatRouter);
|
|
protectedRouter.route('/pipeline-jobs', pipelineJobsRouter);
|
|
protectedRouter.route('/jobs', pipelineJobsRouter); // unified jobs API (script + pipeline); /pipeline-jobs kept for the existing UI
|
|
protectedRouter.route('/desktop', desktopRouter);
|
|
|
|
honoServer.route('/api', protectedRouter);
|
|
|
|
honoServer.onError((error, ctx) => {
|
|
if (error instanceof CustomError) {
|
|
if (error.returnValue) {
|
|
if (typeof error.returnValue === 'string') {
|
|
return ctx.text(error.returnValue, error.statusCode);
|
|
} else {
|
|
return ctx.json(error.returnValue, error.statusCode);
|
|
}
|
|
}
|
|
return ctx.text(error.message, error.statusCode);
|
|
}
|
|
|
|
console.error('Unexpected error:', error.message);
|
|
console.log(error.stack);
|
|
return ctx.text('Internal Server Error', 500);
|
|
});
|