mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-06 17:08:10 +00:00
3e01daf76f
* feat(stack): per-stack activity timeline with actor attribution Adds an Activity tab to the Stack Anatomy panel showing a timestamped event log for each stack: deploys, restarts, starts, stops, and image updates, attributed to the user who triggered them or 'system' for automated actions. Backend: - Extends notification_history with actor_username column (idempotent migration) and a partial composite index on (node_id, stack_name, timestamp DESC) for efficient per-stack lookups. - NotificationService.dispatchAlert() accepts an optional actor that is written to the new column. - Success-side dispatchAlert calls added after deploy, bulkContainerOp (start/stop/restart), and update handlers in routes/stacks.ts so user-initiated operations are recorded, not just failures. - New GET /api/stacks/:stackName/activity?limit&before endpoint with stack:read permission gate and cursor-based pagination. Frontend: - StackAnatomyPanel grows an Anatomy / Activity tab pair using the existing Tabs primitive. - StackActivityTimeline fetches the initial 50 events, paginates on demand, and prepends live events arriving over the existing WS notifications stream without duplicates. - NotificationPanel bell dropdown suppresses user-initiated success events (start/stop/restart/deploy/update triggered by a real user), keeping the tray focused on alerts and system events. * docs(stack): add stack activity timeline feature page and internal arch docs * fix(test): add actor_username to notification-routing history assertions dispatchAlert now passes actor_username to addNotificationHistory after the activity timeline PR added the column. Update the two exact-match assertions that were failing because the expected object shape was missing this field.
157 lines
6.7 KiB
TypeScript
157 lines
6.7 KiB
TypeScript
import express, { Request, Response } from 'express';
|
|
import './types/express';
|
|
import { authGate, auditLog } from './middleware/authGate';
|
|
import { enforceApiTokenScope } from './middleware/apiTokenScope';
|
|
import { errorHandler } from './middleware/errorHandler';
|
|
import { createApp } from './app';
|
|
import { createRemoteProxyMiddleware } from './proxy/remoteNodeProxy';
|
|
import { createServer } from './server';
|
|
import { attachUpgrade } from './websocket/upgradeHandler';
|
|
import { startServer } from './bootstrap/startup';
|
|
import { installShutdownHandlers } from './bootstrap/shutdown';
|
|
import { metaRouter } from './routes/meta';
|
|
import { authRouter } from './routes/auth';
|
|
import { mfaRouter } from './routes/mfa';
|
|
import { ssoRouter } from './routes/sso';
|
|
import { licenseRouter, systemUpdateRouter } from './routes/license';
|
|
import { webhooksRouter } from './routes/webhooks';
|
|
import { usersRouter } from './routes/users';
|
|
import { gitSourcesRouter, stackGitSourceRouter } from './routes/gitSources';
|
|
import { fleetRouter } from './routes/fleet';
|
|
import { cloudBackupRouter } from './routes/cloudBackup';
|
|
import { permissionsRouter } from './routes/permissions';
|
|
import { convertRouter } from './routes/convert';
|
|
import { alertsRouter } from './routes/alerts';
|
|
import { labelsRouter, stackLabelsRouter } from './routes/labels';
|
|
import { apiTokensRouter } from './routes/apiTokens';
|
|
import { auditLogRouter } from './routes/auditLog';
|
|
import { settingsRouter } from './routes/settings';
|
|
import { scheduledTasksRouter } from './routes/scheduledTasks';
|
|
import { agentsRouter } from './routes/agents';
|
|
import { metricsRouter } from './routes/metrics';
|
|
import { imageUpdatesRouter, autoUpdateRouter } from './routes/imageUpdates';
|
|
import { autoHealRouter } from './routes/autoHeal';
|
|
import { notificationsRouter, notificationRoutesRouter } from './routes/notifications';
|
|
import { consoleRouter } from './routes/console';
|
|
import { ssoConfigRouter } from './routes/ssoConfig';
|
|
import { registriesRouter } from './routes/registries';
|
|
import { systemMaintenanceRouter } from './routes/systemMaintenance';
|
|
import { templatesRouter } from './routes/templates';
|
|
import { securityRouter } from './routes/security';
|
|
import { dashboardRouter } from './routes/dashboard';
|
|
import { containersRouter, portsRouter } from './routes/containers';
|
|
import { nodesRouter } from './routes/nodes';
|
|
import { stacksRouter } from './routes/stacks';
|
|
import { stackActivityRouter } from './routes/stackActivity';
|
|
|
|
// Suppress [DEP0060] DeprecationWarning emitted by http-proxy@1.18.1 which calls
|
|
// util._extend internally. The warning fires at runtime when createProxyServer() is
|
|
// first invoked (NOT at import time), so intercepting process.emitWarning here -
|
|
// before the proxy instances are created below - fully prevents it.
|
|
// http-proxy has no compatible update; this suppression is intentional and safe.
|
|
const _origEmitWarning = process.emitWarning.bind(process);
|
|
(process as any).emitWarning = (warning: any, ...args: any[]) => {
|
|
const code = typeof args[0] === 'object' ? args[0]?.code : args[1];
|
|
if (code === 'DEP0060') return;
|
|
_origEmitWarning(warning, ...args);
|
|
};
|
|
|
|
const app = createApp();
|
|
|
|
// Public /api/health and /api/meta (no auth). Mounted before authGate.
|
|
app.use('/api', metaRouter);
|
|
|
|
// Auth / MFA / SSO routers. Mounted before authGate because some paths are
|
|
// public (login, setup, SSO callbacks); handlers that need auth use
|
|
// authMiddleware directly.
|
|
app.use('/api/auth', authRouter);
|
|
app.use('/api/auth', mfaRouter);
|
|
app.use('/api/auth/sso', ssoRouter);
|
|
|
|
// Auth gate on all /api/* routes (exempts /auth/* and webhook triggers).
|
|
app.use('/api', authGate);
|
|
|
|
// Audit-log every mutating /api/* action (POST/PUT/DELETE/PATCH).
|
|
app.use('/api', auditLog);
|
|
|
|
app.use('/api', enforceApiTokenScope);
|
|
|
|
// Remote Node HTTP Proxy (see proxy/remoteNodeProxy.ts). Mounted BEFORE the
|
|
// per-group routers so a request targeting a remote node short-circuits into
|
|
// the proxy instead of hitting a local handler that would read local state.
|
|
// Gateway-level paths (auth, nodes, license, fleet, webhooks, meta) are listed
|
|
// in helpers/proxyExemptPaths.ts and bypass the proxy back to the local
|
|
// handlers below.
|
|
app.use('/api/', createRemoteProxyMiddleware());
|
|
|
|
app.use('/api/license', licenseRouter);
|
|
app.use('/api/system', systemUpdateRouter);
|
|
app.use('/api/permissions', permissionsRouter);
|
|
app.use('/api/convert', convertRouter);
|
|
app.use('/api/alerts', alertsRouter);
|
|
app.use('/api/labels', labelsRouter);
|
|
app.use('/api/stacks', stackLabelsRouter);
|
|
app.use('/api/api-tokens', apiTokensRouter);
|
|
app.use('/api/audit-log', auditLogRouter);
|
|
app.use('/api/fleet', fleetRouter);
|
|
app.use('/api/cloud-backup', cloudBackupRouter);
|
|
app.use('/api/webhooks', webhooksRouter);
|
|
app.use('/api/users', usersRouter);
|
|
app.use('/api/git-sources', gitSourcesRouter);
|
|
app.use('/api/stacks', stackGitSourceRouter);
|
|
app.use('/api/settings', settingsRouter);
|
|
app.use('/api/scheduled-tasks', scheduledTasksRouter);
|
|
app.use('/api/agents', agentsRouter);
|
|
app.use('/api', metricsRouter);
|
|
app.use('/api/image-updates', imageUpdatesRouter);
|
|
app.use('/api/auto-update', autoUpdateRouter);
|
|
app.use('/api/auto-heal', autoHealRouter);
|
|
app.use('/api/notifications', notificationsRouter);
|
|
app.use('/api/notification-routes', notificationRoutesRouter);
|
|
app.use('/api/system', consoleRouter);
|
|
app.use('/api/sso/config', ssoConfigRouter);
|
|
app.use('/api/registries', registriesRouter);
|
|
app.use('/api/system', systemMaintenanceRouter);
|
|
app.use('/api/templates', templatesRouter);
|
|
app.use('/api/security', securityRouter);
|
|
app.use('/api/containers', containersRouter);
|
|
app.use('/api/ports', portsRouter);
|
|
app.use('/api/dashboard', dashboardRouter);
|
|
app.use('/api/nodes', nodesRouter);
|
|
app.use('/api/stacks', stackActivityRouter);
|
|
app.use('/api/stacks', stacksRouter);
|
|
|
|
const { server, wss, pilotTunnelWss } = createServer(app);
|
|
attachUpgrade(server, { wss, pilotTunnelWss });
|
|
|
|
// Static / SPA fallback. Production serves the built frontend; dev returns a
|
|
// JSON 404 for unmatched /api paths to prevent fetch hangs.
|
|
if (process.env.NODE_ENV === 'production') {
|
|
app.use(express.static('public'));
|
|
app.use((req: Request, res: Response) => {
|
|
if (!req.path.startsWith('/api')) {
|
|
res.sendFile('index.html', { root: 'public' });
|
|
} else {
|
|
res.status(404).json({ error: 'API endpoint not found' });
|
|
}
|
|
});
|
|
} else {
|
|
app.use((req: Request, res: Response) => {
|
|
if (req.path.startsWith('/api')) {
|
|
res.status(404).json({ error: 'API endpoint not found' });
|
|
}
|
|
});
|
|
}
|
|
|
|
// Central error handler: must be registered after all routes and static.
|
|
app.use(errorHandler);
|
|
|
|
installShutdownHandlers(server);
|
|
|
|
if (require.main === module) {
|
|
void startServer(server);
|
|
}
|
|
|
|
// Exports used by tests (supertest requires the http.Server instance).
|
|
export { app, server };
|