mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-08-28 02:44:59 +00:00
fix: replace console.* with structured pino logger, fix ESLint errors in k6 load tests
This commit is contained in:
parent
76b93ebe85
commit
8c892d7c22
32 changed files with 406 additions and 296 deletions
|
|
@ -51,6 +51,19 @@ export default [
|
|||
},
|
||||
},
|
||||
|
||||
// k6 load-test files (ES module syntax, k6 globals)
|
||||
{
|
||||
files: ['load-tests/**/*.js'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
__ENV: 'readonly',
|
||||
console: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// React/TypeScript files (web)
|
||||
{
|
||||
files: ['web/src/**/*.tsx', 'web/src/**/*.ts'],
|
||||
|
|
|
|||
|
|
@ -31,7 +31,11 @@ describe('ConfigService', () => {
|
|||
describe('getConfig', () => {
|
||||
it('should return default config when no file exists', async () => {
|
||||
// Remove the file if it exists
|
||||
try { await fs.unlink(configFile); } catch {}
|
||||
try {
|
||||
await fs.unlink(configFile);
|
||||
} catch {
|
||||
/* file may not exist */
|
||||
}
|
||||
const config = await service.getConfig();
|
||||
expect(config).toBeDefined();
|
||||
expect(config.repos).toEqual([]);
|
||||
|
|
@ -41,7 +45,10 @@ describe('ConfigService', () => {
|
|||
|
||||
it('should create config file with defaults when missing', async () => {
|
||||
await service.getConfig();
|
||||
const exists = await fs.access(configFile).then(() => true).catch(() => false);
|
||||
const exists = await fs
|
||||
.access(configFile)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -67,11 +74,14 @@ describe('ConfigService', () => {
|
|||
|
||||
it('should merge feature defaults for backward compat', async () => {
|
||||
// Write config without features
|
||||
await fs.writeFile(configFile, JSON.stringify({
|
||||
repos: [],
|
||||
agents: [],
|
||||
defaultAgent: 'claude-code',
|
||||
}));
|
||||
await fs.writeFile(
|
||||
configFile,
|
||||
JSON.stringify({
|
||||
repos: [],
|
||||
agents: [],
|
||||
defaultAgent: 'claude-code',
|
||||
})
|
||||
);
|
||||
|
||||
const config = await service.getConfig();
|
||||
expect(config.features).toBeDefined();
|
||||
|
|
@ -99,7 +109,10 @@ describe('ConfigService', () => {
|
|||
defaultAgent: 'claude-code',
|
||||
} as any);
|
||||
|
||||
const exists = await fs.access(newFile).then(() => true).catch(() => false);
|
||||
const exists = await fs
|
||||
.access(newFile)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(exists).toBe(true);
|
||||
newService.dispose();
|
||||
});
|
||||
|
|
@ -123,10 +136,13 @@ describe('ConfigService', () => {
|
|||
describe('addRepo', () => {
|
||||
it('should reject duplicate repo names', async () => {
|
||||
// Write a config with one repo
|
||||
await fs.writeFile(configFile, JSON.stringify({
|
||||
repos: [{ name: 'existing', path: '/tmp' }],
|
||||
agents: [],
|
||||
}));
|
||||
await fs.writeFile(
|
||||
configFile,
|
||||
JSON.stringify({
|
||||
repos: [{ name: 'existing', path: '/tmp' }],
|
||||
agents: [],
|
||||
})
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.addRepo({ name: 'existing', path: '/tmp/other' } as any)
|
||||
|
|
@ -136,26 +152,32 @@ describe('ConfigService', () => {
|
|||
|
||||
describe('updateRepo', () => {
|
||||
it('should reject update for non-existent repo', async () => {
|
||||
await fs.writeFile(configFile, JSON.stringify({
|
||||
repos: [],
|
||||
agents: [],
|
||||
}));
|
||||
await fs.writeFile(
|
||||
configFile,
|
||||
JSON.stringify({
|
||||
repos: [],
|
||||
agents: [],
|
||||
})
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.updateRepo('nonexistent', { path: '/new/path' })
|
||||
).rejects.toThrow('not found');
|
||||
await expect(service.updateRepo('nonexistent', { path: '/new/path' })).rejects.toThrow(
|
||||
'not found'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeRepo', () => {
|
||||
it('should remove existing repo', async () => {
|
||||
await fs.writeFile(configFile, JSON.stringify({
|
||||
repos: [
|
||||
{ name: 'keep', path: '/tmp/keep' },
|
||||
{ name: 'remove', path: '/tmp/remove' },
|
||||
],
|
||||
agents: [],
|
||||
}));
|
||||
await fs.writeFile(
|
||||
configFile,
|
||||
JSON.stringify({
|
||||
repos: [
|
||||
{ name: 'keep', path: '/tmp/keep' },
|
||||
{ name: 'remove', path: '/tmp/remove' },
|
||||
],
|
||||
agents: [],
|
||||
})
|
||||
);
|
||||
|
||||
const config = await service.removeRepo('remove');
|
||||
expect(config.repos).toHaveLength(1);
|
||||
|
|
@ -163,10 +185,13 @@ describe('ConfigService', () => {
|
|||
});
|
||||
|
||||
it('should reject removal of non-existent repo', async () => {
|
||||
await fs.writeFile(configFile, JSON.stringify({
|
||||
repos: [],
|
||||
agents: [],
|
||||
}));
|
||||
await fs.writeFile(
|
||||
configFile,
|
||||
JSON.stringify({
|
||||
repos: [],
|
||||
agents: [],
|
||||
})
|
||||
);
|
||||
|
||||
await expect(service.removeRepo('ghost')).rejects.toThrow('not found');
|
||||
});
|
||||
|
|
@ -212,11 +237,14 @@ describe('ConfigService', () => {
|
|||
it('should safely handle config files without prototype pollution', async () => {
|
||||
// JSON.parse produces a plain object even with __proto__ key
|
||||
// The deep merge function has defense-in-depth checks
|
||||
await fs.writeFile(configFile, JSON.stringify({
|
||||
repos: [],
|
||||
agents: [],
|
||||
defaultAgent: 'claude-code',
|
||||
}));
|
||||
await fs.writeFile(
|
||||
configFile,
|
||||
JSON.stringify({
|
||||
repos: [],
|
||||
agents: [],
|
||||
defaultAgent: 'claude-code',
|
||||
})
|
||||
);
|
||||
|
||||
const config = await service.getConfig();
|
||||
// Verify no prototype pollution occurred
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('security');
|
||||
|
||||
// Security config file location
|
||||
const DATA_DIR = process.env.VERITAS_DATA_DIR || path.join(process.cwd(), '.veritas-kanban');
|
||||
|
|
@ -71,7 +73,7 @@ export function getSecurityConfig(): SecurityConfig {
|
|||
return cachedConfig!;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading security config:', err);
|
||||
log.error({ err: err }, 'Error loading security config');
|
||||
}
|
||||
|
||||
// Default config
|
||||
|
|
@ -113,7 +115,7 @@ export function getJwtSecret(): string {
|
|||
// 4. Runtime-generated (ephemeral — sessions won't survive restart)
|
||||
if (!runtimeJwtSecret) {
|
||||
runtimeJwtSecret = crypto.randomBytes(64).toString('hex');
|
||||
console.warn(
|
||||
log.warn(
|
||||
'JWT secret generated at runtime. Set VERITAS_JWT_SECRET env var for persistence across restarts.'
|
||||
);
|
||||
}
|
||||
|
|
@ -233,9 +235,7 @@ export function rotateJwtSecret(gracePeriodMs: number = SECRET_GRACE_PERIOD_MS):
|
|||
};
|
||||
saveSecurityConfig(updatedConfig);
|
||||
|
||||
console.log(
|
||||
`JWT secret rotated to version ${newVersion}. ${prunedCount} expired secret(s) pruned.`
|
||||
);
|
||||
log.info(`JWT secret rotated to version ${newVersion}. ${prunedCount} expired secret(s) pruned.`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -305,9 +305,9 @@ export function saveSecurityConfig(config: SecurityConfig): void {
|
|||
cachedConfig = config;
|
||||
lastLoadTime = Date.now();
|
||||
|
||||
console.log('Security config saved');
|
||||
log.info('Security config saved');
|
||||
} catch (err) {
|
||||
console.error('Error saving security config:', err);
|
||||
log.error({ err: err }, 'Error saving security config');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
|
@ -353,7 +353,7 @@ export function resetSecurityConfig(): void {
|
|||
};
|
||||
saveSecurityConfig(newConfig);
|
||||
runtimeJwtSecret = null;
|
||||
console.log('Security config reset. Next load will show setup screen.');
|
||||
log.info('Security config reset. Next load will show setup screen.');
|
||||
}
|
||||
|
||||
/** Warning about JWT secret configuration */
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import {
|
|||
statusHistoryService,
|
||||
type AgentStatusState as HistoryStatusState,
|
||||
} from '../services/status-history-service.js';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('agent-status');
|
||||
|
||||
const router: RouterType = Router();
|
||||
|
||||
|
|
@ -41,14 +43,14 @@ function loadPersistedStatus(): AgentStatus {
|
|||
const parsed = JSON.parse(raw) as AgentStatus;
|
||||
// Validate it has the expected shape
|
||||
if (parsed.status && typeof parsed.subAgentCount === 'number') {
|
||||
console.log(
|
||||
log.info(
|
||||
`[AgentStatus] Restored persisted status: ${parsed.status} (subAgents: ${parsed.subAgentCount})`
|
||||
);
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.warn('[AgentStatus] Could not load persisted status, starting fresh');
|
||||
log.warn('[AgentStatus] Could not load persisted status, starting fresh');
|
||||
}
|
||||
return {
|
||||
status: 'idle',
|
||||
|
|
@ -68,7 +70,7 @@ function persistStatus(status: AgentStatus): void {
|
|||
}
|
||||
fs.writeFileSync(STATUS_FILE, JSON.stringify(status, null, 2), 'utf-8');
|
||||
} catch (err) {
|
||||
console.warn('[AgentStatus] Failed to persist status:', err);
|
||||
log.warn({ data: err }, '[AgentStatus] Failed to persist status');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +132,7 @@ function resetIdleTimeout(): void {
|
|||
};
|
||||
persistStatus(currentStatus);
|
||||
broadcastAgentStatusChange();
|
||||
console.log('[AgentStatus] Auto-reset to idle after timeout');
|
||||
log.info('[AgentStatus] Auto-reset to idle after timeout');
|
||||
}, IDLE_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +159,7 @@ export function updateAgentStatus(update: Partial<AgentStatus>): AgentStatus {
|
|||
update.subAgentCount ?? currentStatus.subAgentCount
|
||||
)
|
||||
.catch((err) => {
|
||||
console.error('[AgentStatus] Failed to log status change:', err);
|
||||
log.error({ err: err }, '[AgentStatus] Failed to log status change');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { TaskService } from '../services/task-service.js';
|
|||
import { getAttachmentService } from '../services/attachment-service.js';
|
||||
import { getTextExtractionService } from '../services/text-extraction-service.js';
|
||||
import type { Attachment } from '@veritas-kanban/shared';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('attachments');
|
||||
|
||||
const router: RouterType = Router();
|
||||
const taskService = new TaskService();
|
||||
|
|
@ -47,15 +49,17 @@ router.post('/:id/attachments', upload.array('files', 20), async (req: Request,
|
|||
for (const file of files) {
|
||||
try {
|
||||
// Save attachment (includes magic-byte MIME validation)
|
||||
const attachment = await attachmentService.saveAttachment(
|
||||
taskId,
|
||||
file,
|
||||
[...currentAttachments, ...newAttachments]
|
||||
);
|
||||
const attachment = await attachmentService.saveAttachment(taskId, file, [
|
||||
...currentAttachments,
|
||||
...newAttachments,
|
||||
]);
|
||||
|
||||
// Extract text using the validated MIME type
|
||||
const filepath = attachmentService.getAttachmentPath(taskId, attachment.filename);
|
||||
const extractedText = await textExtractionService.extractText(filepath, attachment.mimeType);
|
||||
const extractedText = await textExtractionService.extractText(
|
||||
filepath,
|
||||
attachment.mimeType
|
||||
);
|
||||
|
||||
// Save extracted text if available
|
||||
if (extractedText) {
|
||||
|
|
@ -65,7 +69,7 @@ router.post('/:id/attachments', upload.array('files', 20), async (req: Request,
|
|||
newAttachments.push(attachment);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error(`Rejected file "${file.originalname}":`, message);
|
||||
log.error({ err: message }, `Rejected file "${file.originalname}"`);
|
||||
rejectedFiles.push({ filename: file.originalname, error: message });
|
||||
// Continue with other files
|
||||
}
|
||||
|
|
@ -92,7 +96,7 @@ router.post('/:id/attachments', upload.array('files', 20), async (req: Request,
|
|||
...(rejectedFiles.length > 0 && { rejected: rejectedFiles }),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
log.error({ err: error }, 'Upload error');
|
||||
res.status(500).json({ error: 'Failed to upload attachments' });
|
||||
}
|
||||
});
|
||||
|
|
@ -112,7 +116,7 @@ router.get('/:id/attachments', async (req: Request, res: Response) => {
|
|||
|
||||
res.json(task.attachments || []);
|
||||
} catch (error) {
|
||||
console.error('List attachments error:', error);
|
||||
log.error({ err: error }, 'List attachments error');
|
||||
res.status(500).json({ error: 'Failed to list attachments' });
|
||||
}
|
||||
});
|
||||
|
|
@ -138,7 +142,7 @@ router.get('/:id/attachments/:attId', async (req: Request, res: Response) => {
|
|||
|
||||
res.json(attachment);
|
||||
} catch (error) {
|
||||
console.error('Get attachment error:', error);
|
||||
log.error({ err: error }, 'Get attachment error');
|
||||
res.status(500).json({ error: 'Failed to get attachment' });
|
||||
}
|
||||
});
|
||||
|
|
@ -163,12 +167,15 @@ router.get('/:id/attachments/:attId/download', async (req: Request, res: Respons
|
|||
}
|
||||
|
||||
const filepath = attachmentService.getAttachmentPath(taskId, attachment.filename);
|
||||
|
||||
|
||||
res.setHeader('Content-Type', attachment.mimeType);
|
||||
res.setHeader('Content-Disposition', contentDisposition(attachment.originalName, { type: 'attachment' }));
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
contentDisposition(attachment.originalName, { type: 'attachment' })
|
||||
);
|
||||
res.sendFile(filepath);
|
||||
} catch (error) {
|
||||
console.error('Download error:', error);
|
||||
log.error({ err: error }, 'Download error');
|
||||
res.status(500).json({ error: 'Failed to download attachment' });
|
||||
}
|
||||
});
|
||||
|
|
@ -193,14 +200,14 @@ router.get('/:id/attachments/:attId/text', async (req: Request, res: Response) =
|
|||
}
|
||||
|
||||
const text = await attachmentService.getExtractedText(taskId, attId);
|
||||
|
||||
|
||||
res.json({
|
||||
attachmentId: attId,
|
||||
text,
|
||||
hasText: text !== null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get text error:', error);
|
||||
log.error({ err: error }, 'Get text error');
|
||||
res.status(500).json({ error: 'Failed to get extracted text' });
|
||||
}
|
||||
});
|
||||
|
|
@ -235,7 +242,7 @@ router.delete('/:id/attachments/:attId', async (req: Request, res: Response) =>
|
|||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Delete attachment error:', error);
|
||||
log.error({ err: error }, 'Delete attachment error');
|
||||
res.status(500).json({ error: 'Failed to delete attachment' });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { Router, type Router as RouterType } from 'express';
|
|||
import { z } from 'zod';
|
||||
import { ConfigService } from '../services/config-service.js';
|
||||
import type { RepoConfig, AgentConfig, AgentType } from '@veritas-kanban/shared';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('config');
|
||||
|
||||
const router: RouterType = Router();
|
||||
const configService = new ConfigService();
|
||||
|
|
@ -39,7 +41,7 @@ router.get('/', async (_req, res) => {
|
|||
const config = await configService.getConfig();
|
||||
res.json(config);
|
||||
} catch (error) {
|
||||
console.error('Error getting config:', error);
|
||||
log.error({ err: error }, 'Error getting config');
|
||||
res.status(500).json({ error: 'Failed to get config' });
|
||||
}
|
||||
});
|
||||
|
|
@ -50,7 +52,7 @@ router.get('/repos', async (_req, res) => {
|
|||
const config = await configService.getConfig();
|
||||
res.json(config.repos);
|
||||
} catch (error) {
|
||||
console.error('Error listing repos:', error);
|
||||
log.error({ err: error }, 'Error listing repos');
|
||||
res.status(500).json({ error: 'Failed to list repos' });
|
||||
}
|
||||
});
|
||||
|
|
@ -65,7 +67,7 @@ router.post('/repos', async (req, res) => {
|
|||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ error: 'Validation failed', details: error.errors });
|
||||
}
|
||||
console.error('Error adding repo:', error);
|
||||
log.error({ err: error }, 'Error adding repo');
|
||||
res.status(400).json({ error: error.message || 'Failed to add repo' });
|
||||
}
|
||||
});
|
||||
|
|
@ -80,7 +82,7 @@ router.patch('/repos/:name', async (req, res) => {
|
|||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ error: 'Validation failed', details: error.errors });
|
||||
}
|
||||
console.error('Error updating repo:', error);
|
||||
log.error({ err: error }, 'Error updating repo');
|
||||
res.status(400).json({ error: error.message || 'Failed to update repo' });
|
||||
}
|
||||
});
|
||||
|
|
@ -91,7 +93,7 @@ router.delete('/repos/:name', async (req, res) => {
|
|||
const config = await configService.removeRepo(req.params.name);
|
||||
res.json(config);
|
||||
} catch (error: any) {
|
||||
console.error('Error removing repo:', error);
|
||||
log.error({ err: error }, 'Error removing repo');
|
||||
res.status(400).json({ error: error.message || 'Failed to remove repo' });
|
||||
}
|
||||
});
|
||||
|
|
@ -116,7 +118,7 @@ router.get('/repos/:name/branches', async (req, res) => {
|
|||
const branches = await configService.getRepoBranches(req.params.name);
|
||||
res.json(branches);
|
||||
} catch (error: any) {
|
||||
console.error('Error getting branches:', error);
|
||||
log.error({ err: error }, 'Error getting branches');
|
||||
res.status(400).json({ error: error.message || 'Failed to get branches' });
|
||||
}
|
||||
});
|
||||
|
|
@ -127,7 +129,7 @@ router.get('/agents', async (_req, res) => {
|
|||
const config = await configService.getConfig();
|
||||
res.json(config.agents);
|
||||
} catch (error) {
|
||||
console.error('Error listing agents:', error);
|
||||
log.error({ err: error }, 'Error listing agents');
|
||||
res.status(500).json({ error: 'Failed to list agents' });
|
||||
}
|
||||
});
|
||||
|
|
@ -142,7 +144,7 @@ router.put('/agents', async (req, res) => {
|
|||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ error: 'Validation failed', details: error.errors });
|
||||
}
|
||||
console.error('Error updating agents:', error);
|
||||
log.error({ err: error }, 'Error updating agents');
|
||||
res.status(500).json({ error: 'Failed to update agents' });
|
||||
}
|
||||
});
|
||||
|
|
@ -157,7 +159,7 @@ router.put('/default-agent', async (req, res) => {
|
|||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ error: 'Validation failed', details: error.errors });
|
||||
}
|
||||
console.error('Error setting default agent:', error);
|
||||
log.error({ err: error }, 'Error setting default agent');
|
||||
res.status(500).json({ error: 'Failed to set default agent' });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { Router } from 'express';
|
|||
import { z } from 'zod';
|
||||
import type { ManagedListItem } from '@veritas-kanban/shared';
|
||||
import type { ManagedListService } from '../services/managed-list-service.js';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('managed-list-routes');
|
||||
|
||||
/**
|
||||
* Create a generic Express router for a ManagedListService instance
|
||||
|
|
@ -20,7 +22,7 @@ export function createManagedListRouter<T extends ManagedListItem>(
|
|||
const items = await service.list(includeHidden);
|
||||
res.json(items);
|
||||
} catch (err) {
|
||||
console.error('Error listing items:', err);
|
||||
log.error({ err: err }, 'Error listing items');
|
||||
res.status(500).json({ error: 'Failed to list items' });
|
||||
}
|
||||
});
|
||||
|
|
@ -34,7 +36,7 @@ export function createManagedListRouter<T extends ManagedListItem>(
|
|||
}
|
||||
res.json(item);
|
||||
} catch (err) {
|
||||
console.error('Error getting item:', err);
|
||||
log.error({ err: err }, 'Error getting item');
|
||||
res.status(500).json({ error: 'Failed to get item' });
|
||||
}
|
||||
});
|
||||
|
|
@ -44,11 +46,11 @@ export function createManagedListRouter<T extends ManagedListItem>(
|
|||
try {
|
||||
// Validate with custom schema if provided
|
||||
const data = createSchema ? createSchema.parse(req.body) : req.body;
|
||||
|
||||
|
||||
const item = await service.create(data);
|
||||
res.status(201).json(item);
|
||||
} catch (err: any) {
|
||||
console.error('Error creating item:', err);
|
||||
log.error({ err: err }, 'Error creating item');
|
||||
if (err.name === 'ZodError') {
|
||||
return res.status(400).json({ error: 'Validation error', details: err.errors });
|
||||
}
|
||||
|
|
@ -61,14 +63,14 @@ export function createManagedListRouter<T extends ManagedListItem>(
|
|||
try {
|
||||
// Validate with custom schema if provided
|
||||
const data = updateSchema ? updateSchema.parse(req.body) : req.body;
|
||||
|
||||
|
||||
const item = await service.update(req.params.id, data);
|
||||
if (!item) {
|
||||
return res.status(404).json({ error: 'Item not found' });
|
||||
}
|
||||
res.json(item);
|
||||
} catch (err: any) {
|
||||
console.error('Error updating item:', err);
|
||||
log.error({ err: err }, 'Error updating item');
|
||||
if (err.name === 'ZodError') {
|
||||
return res.status(400).json({ error: 'Validation error', details: err.errors });
|
||||
}
|
||||
|
|
@ -81,20 +83,20 @@ export function createManagedListRouter<T extends ManagedListItem>(
|
|||
try {
|
||||
const force = req.query.force === 'true';
|
||||
const result = await service.delete(req.params.id, force);
|
||||
|
||||
|
||||
if (!result.deleted) {
|
||||
if (result.referenceCount !== undefined && result.referenceCount > 0) {
|
||||
return res.status(400).json({
|
||||
return res.status(400).json({
|
||||
error: 'Cannot delete item with references',
|
||||
referenceCount: result.referenceCount,
|
||||
});
|
||||
}
|
||||
return res.status(400).json({ error: 'Cannot delete default item or item not found' });
|
||||
}
|
||||
|
||||
|
||||
res.status(204).send();
|
||||
} catch (err) {
|
||||
console.error('Error deleting item:', err);
|
||||
log.error({ err: err }, 'Error deleting item');
|
||||
res.status(500).json({ error: 'Failed to delete item' });
|
||||
}
|
||||
});
|
||||
|
|
@ -105,7 +107,7 @@ export function createManagedListRouter<T extends ManagedListItem>(
|
|||
const result = await service.canDelete(req.params.id);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error('Error checking delete permission:', err);
|
||||
log.error({ err: err }, 'Error checking delete permission');
|
||||
res.status(500).json({ error: 'Failed to check delete permission' });
|
||||
}
|
||||
});
|
||||
|
|
@ -116,12 +118,12 @@ export function createManagedListRouter<T extends ManagedListItem>(
|
|||
const schema = z.object({
|
||||
orderedIds: z.array(z.string()),
|
||||
});
|
||||
|
||||
|
||||
const { orderedIds } = schema.parse(req.body);
|
||||
const items = await service.reorder(orderedIds);
|
||||
res.json(items);
|
||||
} catch (err: any) {
|
||||
console.error('Error reordering items:', err);
|
||||
log.error({ err: err }, 'Error reordering items');
|
||||
if (err.name === 'ZodError') {
|
||||
return res.status(400).json({ error: 'Validation error', details: err.errors });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { z } from 'zod';
|
|||
import { ProjectService } from '../services/project-service.js';
|
||||
import { TaskService } from '../services/task-service.js';
|
||||
import { createManagedListRouter } from './managed-list-routes.js';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('projects');
|
||||
|
||||
// Validation schemas
|
||||
const createProjectSchema = z.object({
|
||||
|
|
@ -23,15 +25,11 @@ const taskService = new TaskService();
|
|||
const projectService = new ProjectService(taskService);
|
||||
|
||||
// Initialize service
|
||||
projectService.init().catch(err => {
|
||||
console.error('Failed to initialize ProjectService:', err);
|
||||
projectService.init().catch((err) => {
|
||||
log.error('Failed to initialize ProjectService:', err);
|
||||
});
|
||||
|
||||
// Create router using the generic factory
|
||||
const router = createManagedListRouter(
|
||||
projectService,
|
||||
createProjectSchema,
|
||||
updateProjectSchema
|
||||
);
|
||||
const router = createManagedListRouter(projectService, createProjectSchema, updateProjectSchema);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { FeatureSettingsPatchSchema } from '../schemas/feature-settings-schema.j
|
|||
import { strictRateLimit } from '../middleware/rate-limit.js';
|
||||
import { auditLog } from '../services/audit-service.js';
|
||||
import type { AuthenticatedRequest } from '../middleware/auth.js';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('settings');
|
||||
|
||||
const router: RouterType = Router();
|
||||
const configService = new ConfigService();
|
||||
|
|
@ -39,7 +41,7 @@ router.get('/features', async (_req, res) => {
|
|||
const features = await configService.getFeatureSettings();
|
||||
res.json(features);
|
||||
} catch (error) {
|
||||
console.error('Error getting feature settings:', error);
|
||||
log.error({ err: error }, 'Error getting feature settings');
|
||||
res.status(500).json({ error: 'Failed to get feature settings' });
|
||||
}
|
||||
});
|
||||
|
|
@ -76,7 +78,7 @@ router.patch('/features', strictRateLimit, async (req, res) => {
|
|||
|
||||
res.json(updated);
|
||||
} catch (error) {
|
||||
console.error('Error updating feature settings:', error);
|
||||
log.error({ err: error }, 'Error updating feature settings');
|
||||
res.status(500).json({ error: 'Failed to update feature settings' });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { z } from 'zod';
|
|||
import { SprintService } from '../services/sprint-service.js';
|
||||
import { TaskService } from '../services/task-service.js';
|
||||
import { createManagedListRouter } from './managed-list-routes.js';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('sprints');
|
||||
|
||||
// Validation schemas
|
||||
const createSprintSchema = z.object({
|
||||
|
|
@ -21,15 +23,11 @@ const taskService = new TaskService();
|
|||
const sprintService = new SprintService(taskService);
|
||||
|
||||
// Initialize service
|
||||
sprintService.init().catch(err => {
|
||||
console.error('Failed to initialize SprintService:', err);
|
||||
sprintService.init().catch((err) => {
|
||||
log.error('Failed to initialize SprintService:', err);
|
||||
});
|
||||
|
||||
// Create router using the generic factory
|
||||
const router = createManagedListRouter(
|
||||
sprintService,
|
||||
createSprintSchema,
|
||||
updateSprintSchema
|
||||
);
|
||||
const router = createManagedListRouter(sprintService, createSprintSchema, updateSprintSchema);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { z } from 'zod';
|
|||
import { TaskTypeService } from '../services/task-type-service.js';
|
||||
import { TaskService } from '../services/task-service.js';
|
||||
import { createManagedListRouter } from './managed-list-routes.js';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('task-types');
|
||||
|
||||
// Validation schemas
|
||||
const createTaskTypeSchema = z.object({
|
||||
|
|
@ -23,15 +25,11 @@ const taskService = new TaskService();
|
|||
const taskTypeService = new TaskTypeService(taskService);
|
||||
|
||||
// Initialize service
|
||||
taskTypeService.init().catch(err => {
|
||||
console.error('Failed to initialize TaskTypeService:', err);
|
||||
taskTypeService.init().catch((err) => {
|
||||
log.error('Failed to initialize TaskTypeService:', err);
|
||||
});
|
||||
|
||||
// Create router using the generic factory
|
||||
const router = createManagedListRouter(
|
||||
taskTypeService,
|
||||
createTaskTypeSchema,
|
||||
updateTaskTypeSchema
|
||||
);
|
||||
const router = createManagedListRouter(taskTypeService, createTaskTypeSchema, updateTaskTypeSchema);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import {
|
|||
type TelemetryBulkQuery,
|
||||
type TelemetryExportQuery,
|
||||
} from '../schemas/telemetry-schemas.js';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('telemetry');
|
||||
|
||||
const router: RouterType = Router();
|
||||
|
||||
|
|
@ -28,15 +30,15 @@ const router: RouterType = Router();
|
|||
/**
|
||||
* POST /api/telemetry/events
|
||||
* Ingest telemetry events from external sources (Veritas, Clawdbot, etc.)
|
||||
*
|
||||
*
|
||||
* Accepts: run.started, run.completed, run.error, run.tokens events
|
||||
*
|
||||
*
|
||||
* Request body:
|
||||
* - type: Event type (required)
|
||||
* - taskId: Task ID this event relates to (required)
|
||||
* - agent: Agent name/type (required)
|
||||
* - ...type-specific fields
|
||||
*
|
||||
*
|
||||
* Response: The created event with generated id and timestamp
|
||||
*/
|
||||
router.post(
|
||||
|
|
@ -45,13 +47,13 @@ router.post(
|
|||
asyncHandler(async (req: ValidatedRequest<unknown, unknown, TelemetryEventIngestion>, res) => {
|
||||
const telemetry = getTelemetryService();
|
||||
const eventInput = req.validated.body!;
|
||||
|
||||
|
||||
// Emit the event (adds id and timestamp)
|
||||
const event = await telemetry.emit(eventInput);
|
||||
|
||||
|
||||
// Broadcast to WebSocket clients
|
||||
broadcastTelemetryEvent(event as AnyTelemetryEvent);
|
||||
|
||||
|
||||
// Check for failure events and send alerts (non-blocking)
|
||||
const failureAlertService = getFailureAlertService();
|
||||
if (failureAlertService.isFailureEvent(eventInput)) {
|
||||
|
|
@ -66,15 +68,15 @@ router.post(
|
|||
} catch {
|
||||
// Task not found is fine, we'll use taskId
|
||||
}
|
||||
|
||||
|
||||
await failureAlertService.processEvent(eventInput, taskTitle);
|
||||
} catch (err) {
|
||||
// Graceful failure: log but don't crash
|
||||
console.error('[Telemetry] Failure alert error:', err);
|
||||
log.error({ err: err }, '[Telemetry] Failure alert error');
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
res.status(201).json(event);
|
||||
})
|
||||
);
|
||||
|
|
@ -91,9 +93,9 @@ router.get(
|
|||
asyncHandler(async (req: ValidatedRequest<unknown, TelemetryEventsQuery>, res) => {
|
||||
const telemetry = getTelemetryService();
|
||||
const { type, since, until, taskId, project, limit } = req.validated.query!;
|
||||
|
||||
|
||||
const options: TelemetryQueryOptions = {};
|
||||
|
||||
|
||||
if (type && type.length > 0) {
|
||||
options.type = type.length === 1 ? type[0] : type;
|
||||
}
|
||||
|
|
@ -102,7 +104,7 @@ router.get(
|
|||
if (taskId) options.taskId = taskId;
|
||||
if (project) options.project = project;
|
||||
if (limit) options.limit = limit;
|
||||
|
||||
|
||||
const events = await telemetry.getEvents(options);
|
||||
res.json(events);
|
||||
})
|
||||
|
|
@ -126,7 +128,7 @@ router.get(
|
|||
/**
|
||||
* POST /api/telemetry/events/bulk
|
||||
* Get events for multiple tasks in one request (batch query)
|
||||
*
|
||||
*
|
||||
* Returns: { [taskId]: events[] }
|
||||
*/
|
||||
router.post(
|
||||
|
|
@ -135,15 +137,15 @@ router.post(
|
|||
asyncHandler(async (req: ValidatedRequest<unknown, unknown, TelemetryBulkQuery>, res) => {
|
||||
const telemetry = getTelemetryService();
|
||||
const { taskIds } = req.validated.body!;
|
||||
|
||||
|
||||
const eventsMap = await telemetry.getBulkTaskEvents(taskIds);
|
||||
|
||||
|
||||
// Convert Map to plain object for JSON response
|
||||
const result: Record<string, AnyTelemetryEvent[]> = {};
|
||||
for (const [taskId, events] of eventsMap) {
|
||||
result[taskId] = events;
|
||||
}
|
||||
|
||||
|
||||
res.json(result);
|
||||
})
|
||||
);
|
||||
|
|
@ -152,16 +154,19 @@ router.post(
|
|||
* GET /api/telemetry/status
|
||||
* Get telemetry service status and configuration
|
||||
*/
|
||||
router.get('/status', asyncHandler(async (_req, res) => {
|
||||
const telemetry = getTelemetryService();
|
||||
const config = telemetry.getConfig();
|
||||
|
||||
res.json({
|
||||
enabled: config.enabled,
|
||||
retention: config.retention,
|
||||
traces: config.traces,
|
||||
});
|
||||
}));
|
||||
router.get(
|
||||
'/status',
|
||||
asyncHandler(async (_req, res) => {
|
||||
const telemetry = getTelemetryService();
|
||||
const config = telemetry.getConfig();
|
||||
|
||||
res.json({
|
||||
enabled: config.enabled,
|
||||
retention: config.retention,
|
||||
traces: config.traces,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/telemetry/count
|
||||
|
|
@ -173,13 +178,9 @@ router.get(
|
|||
asyncHandler(async (req: ValidatedRequest<unknown, TelemetryCountQuery>, res) => {
|
||||
const telemetry = getTelemetryService();
|
||||
const { type, since, until } = req.validated.query!;
|
||||
|
||||
const count = await telemetry.countEvents(
|
||||
type.length === 1 ? type[0] : type,
|
||||
since,
|
||||
until
|
||||
);
|
||||
|
||||
|
||||
const count = await telemetry.countEvents(type.length === 1 ? type[0] : type, since, until);
|
||||
|
||||
res.json({ count });
|
||||
})
|
||||
);
|
||||
|
|
@ -187,14 +188,14 @@ router.get(
|
|||
/**
|
||||
* GET /api/telemetry/export
|
||||
* Export telemetry events as CSV or JSON file download
|
||||
*
|
||||
*
|
||||
* Query params:
|
||||
* - format: 'csv' | 'json' (default: 'json')
|
||||
* - taskId: Filter by specific task
|
||||
* - project: Filter by project name
|
||||
* - from: Start date (ISO timestamp)
|
||||
* - to: End date (ISO timestamp)
|
||||
*
|
||||
*
|
||||
* Response: File download with appropriate Content-Disposition header
|
||||
*/
|
||||
router.get(
|
||||
|
|
@ -203,23 +204,23 @@ router.get(
|
|||
asyncHandler(async (req: ValidatedRequest<unknown, TelemetryExportQuery>, res) => {
|
||||
const telemetry = getTelemetryService();
|
||||
const { format, taskId, project, from, to } = req.validated.query!;
|
||||
|
||||
|
||||
// Build query options
|
||||
const options: TelemetryQueryOptions = {};
|
||||
if (taskId) options.taskId = taskId;
|
||||
if (project) options.project = project;
|
||||
if (from) options.since = from;
|
||||
if (to) options.until = to;
|
||||
|
||||
|
||||
// Generate filename with scope and date info
|
||||
const scopeParts: string[] = ['telemetry'];
|
||||
if (taskId) scopeParts.push(`task-${taskId}`);
|
||||
else if (project) scopeParts.push(`project-${project.replace(/[^a-zA-Z0-9-_]/g, '_')}`);
|
||||
else scopeParts.push('full');
|
||||
|
||||
|
||||
const dateStr = new Date().toISOString().slice(0, 10);
|
||||
const filename = `${scopeParts.join('-')}-${dateStr}.${format}`;
|
||||
|
||||
|
||||
if (format === 'csv') {
|
||||
const csvData = await telemetry.exportAsCsv(options);
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { Router, type Router as RouterType } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { TemplateService } from '../services/template-service.js';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('templates');
|
||||
|
||||
const router: RouterType = Router();
|
||||
const templateService = new TemplateService();
|
||||
|
|
@ -44,13 +46,15 @@ const updateTemplateSchema = z.object({
|
|||
name: z.string().min(1).optional(),
|
||||
description: z.string().optional(),
|
||||
category: z.string().optional(),
|
||||
taskDefaults: z.object({
|
||||
type: z.string().optional(),
|
||||
priority: z.enum(['low', 'medium', 'high']).optional(),
|
||||
project: z.string().optional(),
|
||||
descriptionTemplate: z.string().optional(),
|
||||
agent: z.enum(['claude-code', 'amp', 'copilot', 'gemini', 'veritas']).optional(),
|
||||
}).optional(),
|
||||
taskDefaults: z
|
||||
.object({
|
||||
type: z.string().optional(),
|
||||
priority: z.enum(['low', 'medium', 'high']).optional(),
|
||||
project: z.string().optional(),
|
||||
descriptionTemplate: z.string().optional(),
|
||||
agent: z.enum(['claude-code', 'amp', 'copilot', 'gemini', 'veritas']).optional(),
|
||||
})
|
||||
.optional(),
|
||||
subtaskTemplates: z.array(subtaskTemplateSchema).optional(),
|
||||
blueprint: z.array(blueprintTaskSchema).optional(),
|
||||
});
|
||||
|
|
@ -61,7 +65,7 @@ router.get('/', async (_req, res) => {
|
|||
const templates = await templateService.getTemplates();
|
||||
res.json(templates);
|
||||
} catch (error) {
|
||||
console.error('Error listing templates:', error);
|
||||
log.error({ err: error }, 'Error listing templates');
|
||||
res.status(500).json({ error: 'Failed to list templates' });
|
||||
}
|
||||
});
|
||||
|
|
@ -75,7 +79,7 @@ router.get('/:id', async (req, res) => {
|
|||
}
|
||||
res.json(template);
|
||||
} catch (error) {
|
||||
console.error('Error getting template:', error);
|
||||
log.error({ err: error }, 'Error getting template');
|
||||
res.status(500).json({ error: 'Failed to get template' });
|
||||
}
|
||||
});
|
||||
|
|
@ -90,7 +94,7 @@ router.post('/', async (req, res) => {
|
|||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ error: 'Validation failed', details: error.errors });
|
||||
}
|
||||
console.error('Error creating template:', error);
|
||||
log.error({ err: error }, 'Error creating template');
|
||||
res.status(500).json({ error: 'Failed to create template' });
|
||||
}
|
||||
});
|
||||
|
|
@ -108,7 +112,7 @@ router.patch('/:id', async (req, res) => {
|
|||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ error: 'Validation failed', details: error.errors });
|
||||
}
|
||||
console.error('Error updating template:', error);
|
||||
log.error({ err: error }, 'Error updating template');
|
||||
res.status(500).json({ error: 'Failed to update template' });
|
||||
}
|
||||
});
|
||||
|
|
@ -122,7 +126,7 @@ router.delete('/:id', async (req, res) => {
|
|||
}
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error('Error deleting template:', error);
|
||||
log.error({ err: error }, 'Error deleting template');
|
||||
res.status(500).json({ error: 'Failed to delete template' });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { readFile, writeFile, mkdir } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('activity-service');
|
||||
|
||||
export type ActivityType =
|
||||
| 'task_created'
|
||||
|
|
@ -94,7 +96,7 @@ export class ActivityService {
|
|||
activities = [activity, ...activities].slice(0, this.MAX_ACTIVITIES);
|
||||
|
||||
if (activities.length >= this.MAX_ACTIVITIES) {
|
||||
console.warn(
|
||||
log.warn(
|
||||
`[Activity] Activity limit reached (${this.MAX_ACTIVITIES}), trimming oldest entries`
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import mime from 'mime-types';
|
|||
import type { Attachment, AttachmentLimits } from '@veritas-kanban/shared';
|
||||
import { DEFAULT_ATTACHMENT_LIMITS, ALLOWED_MIME_TYPES } from '@veritas-kanban/shared';
|
||||
import { validateMimeType, getAllowedTypesDescription } from './mime-validation.js';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('attachment-service');
|
||||
|
||||
// Default paths - resolve to project root (one level up from server/)
|
||||
const DEFAULT_PROJECT_ROOT = path.resolve(process.cwd(), '..');
|
||||
|
|
@ -67,7 +69,7 @@ export class AttachmentService {
|
|||
!normalizedPath.startsWith(normalizedBase + path.sep) &&
|
||||
normalizedPath !== normalizedBase
|
||||
) {
|
||||
console.error(`Path traversal attempt blocked: ${context}`, { resolvedPath, baseDir });
|
||||
log.error({ err: { resolvedPath, baseDir } }, `Path traversal attempt blocked: ${context}`);
|
||||
throw new Error('Invalid path: access denied');
|
||||
}
|
||||
}
|
||||
|
|
@ -257,7 +259,7 @@ export class AttachmentService {
|
|||
try {
|
||||
await fs.unlink(filepath);
|
||||
} catch (err) {
|
||||
console.error(`Failed to delete attachment file: ${filepath}`, err);
|
||||
log.error({ err: err }, `Failed to delete attachment file: ${filepath}`);
|
||||
}
|
||||
|
||||
// Delete extracted text
|
||||
|
|
@ -318,7 +320,7 @@ export class AttachmentService {
|
|||
} catch (err) {
|
||||
// Ignore if source doesn't exist
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error(`Failed to archive attachments for task ${taskId}:`, err);
|
||||
log.error({ err: err }, `Failed to archive attachments for task ${taskId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -342,7 +344,7 @@ export class AttachmentService {
|
|||
} catch (err) {
|
||||
// Ignore if source doesn't exist
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error(`Failed to restore attachments for task ${taskId}:`, err);
|
||||
log.error({ err: err }, `Failed to restore attachments for task ${taskId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -356,7 +358,7 @@ export class AttachmentService {
|
|||
try {
|
||||
await fs.rm(taskDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.error(`Failed to delete attachments directory for task ${taskId}:`, err);
|
||||
log.error({ err: err }, `Failed to delete attachments directory for task ${taskId}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import { ConfigService } from './config-service.js';
|
|||
import { TaskService } from './task-service.js';
|
||||
import { getBreaker } from './circuit-registry.js';
|
||||
import type { Task, AgentType, TaskAttempt, AttemptStatus } from '@veritas-kanban/shared';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('clawdbot-agent-service');
|
||||
|
||||
const PROJECT_ROOT = path.resolve(process.cwd(), '..');
|
||||
const LOGS_DIR = path.join(PROJECT_ROOT, '.veritas-kanban', 'logs');
|
||||
|
|
@ -189,8 +191,8 @@ export class ClawdbotAgentService {
|
|||
)
|
||||
);
|
||||
|
||||
console.log(`[ClawdbotAgent] Wrote agent request for task ${taskId} to ${requestFile}`);
|
||||
console.log(
|
||||
log.info(`[ClawdbotAgent] Wrote agent request for task ${taskId} to ${requestFile}`);
|
||||
log.info(
|
||||
`[ClawdbotAgent] Veritas should pick this up on next heartbeat or you can trigger manually`
|
||||
);
|
||||
}
|
||||
|
|
@ -204,7 +206,7 @@ export class ClawdbotAgentService {
|
|||
): Promise<void> {
|
||||
const pending = pendingAgents.get(taskId);
|
||||
if (!pending) {
|
||||
console.warn(`[ClawdbotAgent] Received completion for unknown task ${taskId}`);
|
||||
log.warn(`[ClawdbotAgent] Received completion for unknown task ${taskId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -248,7 +250,7 @@ export class ClawdbotAgentService {
|
|||
// Ignore if already deleted
|
||||
}
|
||||
|
||||
console.log(`[ClawdbotAgent] Task ${taskId} completed with status: ${status}`);
|
||||
log.info(`[ClawdbotAgent] Task ${taskId} completed with status: ${status}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { simpleGit } from 'simple-git';
|
||||
import { TaskService } from './task-service.js';
|
||||
import type { Task } from '@veritas-kanban/shared';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('diff-service');
|
||||
|
||||
export interface FileChange {
|
||||
path: string;
|
||||
|
|
@ -55,36 +57,41 @@ export class DiffService {
|
|||
private getLanguageFromPath(filePath: string): string {
|
||||
const ext = filePath.split('.').pop()?.toLowerCase() || '';
|
||||
const langMap: Record<string, string> = {
|
||||
'ts': 'typescript',
|
||||
'tsx': 'tsx',
|
||||
'js': 'javascript',
|
||||
'jsx': 'jsx',
|
||||
'json': 'json',
|
||||
'md': 'markdown',
|
||||
'css': 'css',
|
||||
'scss': 'scss',
|
||||
'html': 'html',
|
||||
'py': 'python',
|
||||
'rs': 'rust',
|
||||
'go': 'go',
|
||||
'rb': 'ruby',
|
||||
'java': 'java',
|
||||
'sh': 'bash',
|
||||
'yaml': 'yaml',
|
||||
'yml': 'yaml',
|
||||
'toml': 'toml',
|
||||
'sql': 'sql',
|
||||
ts: 'typescript',
|
||||
tsx: 'tsx',
|
||||
js: 'javascript',
|
||||
jsx: 'jsx',
|
||||
json: 'json',
|
||||
md: 'markdown',
|
||||
css: 'css',
|
||||
scss: 'scss',
|
||||
html: 'html',
|
||||
py: 'python',
|
||||
rs: 'rust',
|
||||
go: 'go',
|
||||
rb: 'ruby',
|
||||
java: 'java',
|
||||
sh: 'bash',
|
||||
yaml: 'yaml',
|
||||
yml: 'yaml',
|
||||
toml: 'toml',
|
||||
sql: 'sql',
|
||||
};
|
||||
return langMap[ext] || 'plaintext';
|
||||
}
|
||||
|
||||
private parseStatusCode(code: string): FileChange['status'] {
|
||||
switch (code[0]) {
|
||||
case 'A': return 'added';
|
||||
case 'M': return 'modified';
|
||||
case 'D': return 'deleted';
|
||||
case 'R': return 'renamed';
|
||||
default: return 'modified';
|
||||
case 'A':
|
||||
return 'added';
|
||||
case 'M':
|
||||
return 'modified';
|
||||
case 'D':
|
||||
return 'deleted';
|
||||
case 'R':
|
||||
return 'renamed';
|
||||
default:
|
||||
return 'modified';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,21 +106,24 @@ export class DiffService {
|
|||
|
||||
// Get diff against base branch
|
||||
const baseBranch = task.git.baseBranch || 'main';
|
||||
|
||||
|
||||
// Get list of changed files with stats
|
||||
const diffStat = await git.diffSummary([baseBranch]);
|
||||
|
||||
const files: FileChange[] = diffStat.files.map(file => {
|
||||
|
||||
const files: FileChange[] = diffStat.files.map((file) => {
|
||||
const additions = 'insertions' in file ? file.insertions : 0;
|
||||
const deletions = 'deletions' in file ? file.deletions : 0;
|
||||
const isBinary = 'binary' in file && file.binary;
|
||||
|
||||
|
||||
return {
|
||||
path: file.file,
|
||||
status: isBinary ? 'modified' as const : (
|
||||
additions > 0 && deletions === 0 ? 'added' as const :
|
||||
deletions > 0 && additions === 0 ? 'deleted' as const : 'modified' as const
|
||||
),
|
||||
status: isBinary
|
||||
? ('modified' as const)
|
||||
: additions > 0 && deletions === 0
|
||||
? ('added' as const)
|
||||
: deletions > 0 && additions === 0
|
||||
? ('deleted' as const)
|
||||
: ('modified' as const),
|
||||
additions,
|
||||
deletions,
|
||||
};
|
||||
|
|
@ -139,7 +149,7 @@ export class DiffService {
|
|||
|
||||
// Get unified diff for the file
|
||||
const diffOutput = await git.diff([baseBranch, '--', filePath]);
|
||||
|
||||
|
||||
// Get file stats
|
||||
const diffStat = await git.diffSummary([baseBranch, '--', filePath]);
|
||||
const fileStat = diffStat.files[0];
|
||||
|
|
@ -151,7 +161,7 @@ export class DiffService {
|
|||
let status: FileChange['status'] = 'modified';
|
||||
let additions = 0;
|
||||
let deletions = 0;
|
||||
|
||||
|
||||
if (fileStat && 'insertions' in fileStat) {
|
||||
additions = fileStat.insertions;
|
||||
deletions = fileStat.deletions;
|
||||
|
|
@ -172,7 +182,7 @@ export class DiffService {
|
|||
private parseUnifiedDiff(diffOutput: string): DiffHunk[] {
|
||||
const hunks: DiffHunk[] = [];
|
||||
const lines = diffOutput.split('\n');
|
||||
|
||||
|
||||
let currentHunk: DiffHunk | null = null;
|
||||
let oldLineNum = 0;
|
||||
let newLineNum = 0;
|
||||
|
|
@ -180,15 +190,15 @@ export class DiffService {
|
|||
for (const line of lines) {
|
||||
// Match hunk header: @@ -start,count +start,count @@
|
||||
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
||||
|
||||
|
||||
if (hunkMatch) {
|
||||
if (currentHunk) {
|
||||
hunks.push(currentHunk);
|
||||
}
|
||||
|
||||
|
||||
oldLineNum = parseInt(hunkMatch[1], 10);
|
||||
newLineNum = parseInt(hunkMatch[3], 10);
|
||||
|
||||
|
||||
currentHunk = {
|
||||
oldStart: oldLineNum,
|
||||
oldLines: parseInt(hunkMatch[2] || '1', 10),
|
||||
|
|
@ -202,11 +212,13 @@ export class DiffService {
|
|||
if (!currentHunk) continue;
|
||||
|
||||
// Skip diff metadata lines
|
||||
if (line.startsWith('diff --git') ||
|
||||
line.startsWith('index ') ||
|
||||
line.startsWith('---') ||
|
||||
line.startsWith('+++') ||
|
||||
line.startsWith('\\')) {
|
||||
if (
|
||||
line.startsWith('diff --git') ||
|
||||
line.startsWith('index ') ||
|
||||
line.startsWith('---') ||
|
||||
line.startsWith('+++') ||
|
||||
line.startsWith('\\')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -249,7 +261,7 @@ export class DiffService {
|
|||
diffs.push(diff);
|
||||
} catch (e) {
|
||||
// Skip files that can't be diffed (binary, etc.)
|
||||
console.warn(`Could not get diff for ${file.path}:`, e);
|
||||
log.warn({ data: e }, `Could not get diff for ${file.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/**
|
||||
* Failure Alert Service
|
||||
*
|
||||
*
|
||||
* Automatically notifies Teams when agent runs fail.
|
||||
* Detects run.error and run.completed with success:false telemetry events.
|
||||
*
|
||||
*
|
||||
* Features:
|
||||
* - Configurable on/off (default off)
|
||||
* - Deduplication: won't spam for same task retries within 5 min
|
||||
|
|
@ -61,7 +61,7 @@ export class FailureAlertService {
|
|||
isRecentlyAlerted(taskId: string): boolean {
|
||||
const lastAlert = recentAlerts.get(taskId);
|
||||
if (!lastAlert) return false;
|
||||
|
||||
|
||||
const elapsed = Date.now() - lastAlert;
|
||||
return elapsed < this.dedupWindowMs;
|
||||
}
|
||||
|
|
@ -71,7 +71,7 @@ export class FailureAlertService {
|
|||
*/
|
||||
private recordAlert(taskId: string): void {
|
||||
recentAlerts.set(taskId, Date.now());
|
||||
|
||||
|
||||
// Clean up old entries periodically
|
||||
if (recentAlerts.size > 100) {
|
||||
const cutoff = Date.now() - this.dedupWindowMs;
|
||||
|
|
@ -122,14 +122,14 @@ export class FailureAlertService {
|
|||
// Check if notifications are enabled
|
||||
const features = await this.configService.getFeatureSettings();
|
||||
const notifSettings = features.notifications;
|
||||
|
||||
|
||||
if (!notifSettings.enabled || !notifSettings.onAgentFailure) {
|
||||
return { sent: false, reason: 'disabled' };
|
||||
}
|
||||
|
||||
// Check deduplication
|
||||
if (this.isRecentlyAlerted(event.taskId)) {
|
||||
console.log(`[FailureAlert] Skipping duplicate alert for task ${event.taskId}`);
|
||||
log.info(`[FailureAlert] Skipping duplicate alert for task ${event.taskId}`);
|
||||
return { sent: false, reason: 'deduplicated' };
|
||||
}
|
||||
|
||||
|
|
@ -163,13 +163,13 @@ export class FailureAlertService {
|
|||
});
|
||||
|
||||
// Log success
|
||||
console.log(`[FailureAlert] Created notification ${notification.id} for task ${event.taskId}`);
|
||||
log.info(`[FailureAlert] Created notification ${notification.id} for task ${event.taskId}`);
|
||||
|
||||
return { sent: true, notificationId: notification.id };
|
||||
} catch (err) {
|
||||
// Graceful failure: log but don't crash
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[FailureAlert] Failed to send alert: ${errorMsg}`);
|
||||
log.error(`[FailureAlert] Failed to send alert: ${errorMsg}`);
|
||||
return { sent: false, reason: 'error', error: errorMsg };
|
||||
}
|
||||
}
|
||||
|
|
@ -198,7 +198,7 @@ export class FailureAlertService {
|
|||
try {
|
||||
const features = await this.configService.getFeatureSettings();
|
||||
const webhookUrl = features.notifications.webhookUrl;
|
||||
|
||||
|
||||
if (!webhookUrl) {
|
||||
// No webhook configured - notification will be available via polling
|
||||
return false;
|
||||
|
|
@ -213,14 +213,14 @@ export class FailureAlertService {
|
|||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`[FailureAlert] Webhook delivery failed: ${response.status}`);
|
||||
log.warn(`[FailureAlert] Webhook delivery failed: ${response.status}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('[FailureAlert] Webhook delivery successful');
|
||||
log.info('[FailureAlert] Webhook delivery successful');
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn('[FailureAlert] Webhook error:', err instanceof Error ? err.message : err);
|
||||
log.warn({ data: err instanceof Error ? err.message : err }, '[FailureAlert] Webhook error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -264,3 +264,5 @@ export function getFailureAlertService(): FailureAlertService {
|
|||
|
||||
// Re-export ConfigService getter for completeness
|
||||
export { getConfigService } from './config-service.js';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('failure-alert-service');
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { promisify } from 'util';
|
|||
import { ConfigService } from './config-service.js';
|
||||
import { TaskService } from './task-service.js';
|
||||
import { getBreaker } from './circuit-registry.js';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('github-service');
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
|
@ -165,7 +167,7 @@ export class GitHubService {
|
|||
} catch (error: any) {
|
||||
// Ignore if already pushed
|
||||
if (!error.message?.includes('Everything up-to-date')) {
|
||||
console.warn('Push warning:', error.message);
|
||||
log.warn('Push warning:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { existsSync } from 'fs';
|
|||
import { join } from 'path';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { ManagedListItem } from '@veritas-kanban/shared';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('managed-list-service');
|
||||
|
||||
export interface ManagedListServiceConfig<T extends ManagedListItem> {
|
||||
filename: string;
|
||||
|
|
@ -50,7 +52,7 @@ export class ManagedListService<T extends ManagedListItem> {
|
|||
const content = await readFile(this.filePath, 'utf-8');
|
||||
this.items = JSON.parse(content);
|
||||
} catch (err) {
|
||||
console.error('Error loading managed list:', err);
|
||||
log.error({ err: err }, 'Error loading managed list');
|
||||
this.items = [...this.defaults];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ import type {
|
|||
AgentRecommendation,
|
||||
AgentComparisonResult,
|
||||
} from './types.js';
|
||||
import { createLogger } from '../../lib/logger.js';
|
||||
const log = createLogger('dashboard-metrics');
|
||||
|
||||
/**
|
||||
* Get all metrics in one call (for dashboard).
|
||||
|
|
@ -156,7 +158,7 @@ export async function computeAllMetrics(
|
|||
}
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.error(`[Metrics] Error reading ${filePath}:`, error.message);
|
||||
log.error(`[Metrics] Error reading ${filePath}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -300,7 +302,7 @@ export async function computeAllMetrics(
|
|||
}
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.error(`[Metrics] Error reading ${filePath}:`, error.message);
|
||||
log.error(`[Metrics] Error reading ${filePath}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -424,7 +426,7 @@ export async function computeTrends(
|
|||
}
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.error(`[Metrics] Error reading ${filePath}:`, error.message);
|
||||
log.error(`[Metrics] Error reading ${filePath}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -574,7 +576,7 @@ export async function computeAgentComparison(
|
|||
}
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.error(`[Metrics] Error reading ${filePath}:`, error.message);
|
||||
log.error(`[Metrics] Error reading ${filePath}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import type {
|
|||
DurationMetrics,
|
||||
FailedRunDetails,
|
||||
} from './types.js';
|
||||
import { createLogger } from '../../lib/logger.js';
|
||||
const log = createLogger('run-metrics');
|
||||
|
||||
/**
|
||||
* Get run metrics (error rate, success rate) with per-agent breakdown
|
||||
|
|
@ -221,7 +223,7 @@ export async function computeFailedRuns(
|
|||
}
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.error(`[Metrics] Error reading ${filePath}:`, error.message);
|
||||
log.error(`[Metrics] Error reading ${filePath}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import path from 'path';
|
|||
import readline from 'readline';
|
||||
import { createGunzip } from 'zlib';
|
||||
import type { AnyTelemetryEvent, TelemetryEventType, StreamEventHandler } from './types.js';
|
||||
import { createLogger } from '../../lib/logger.js';
|
||||
const log = createLogger('telemetry-reader');
|
||||
|
||||
/**
|
||||
* Get list of event files within a date range (includes .ndjson and .ndjson.gz)
|
||||
|
|
@ -90,7 +92,7 @@ export async function streamEvents<T>(
|
|||
}
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.error(`[Metrics] Error reading ${filePath}:`, error.message);
|
||||
log.error(`[Metrics] Error reading ${filePath}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import type { TokenTelemetryEvent, AnyTelemetryEvent } from '@veritas-kanban/sha
|
|||
import { getPeriodStart, percentile } from './helpers.js';
|
||||
import { getEventFiles, streamEvents, createLineReader } from './telemetry-reader.js';
|
||||
import type { MetricsPeriod, TokenMetrics, TokenAccumulator, BudgetMetrics } from './types.js';
|
||||
import { createLogger } from '../../lib/logger.js';
|
||||
const log = createLogger('token-metrics');
|
||||
|
||||
/**
|
||||
* Get token metrics with per-agent breakdown
|
||||
|
|
@ -155,7 +157,7 @@ export async function computeBudgetMetrics(
|
|||
}
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.error(`[Metrics] Error reading ${filePath}:`, error.message);
|
||||
log.error(`[Metrics] Error reading ${filePath}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { TaskService } from './task-service.js';
|
||||
import type { TaskStatus } from '@veritas-kanban/shared';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('migration-service');
|
||||
|
||||
/**
|
||||
* One-time data migrations that run on server startup.
|
||||
|
|
@ -21,11 +23,11 @@ export class MigrationService {
|
|||
|
||||
/**
|
||||
* Migrate tasks with status "review" to "blocked"
|
||||
*
|
||||
*
|
||||
* Background: The "review" status was removed from the workflow.
|
||||
* Any existing tasks with that status should be converted to "blocked"
|
||||
* so they remain visible and actionable.
|
||||
*
|
||||
*
|
||||
* This migration is idempotent - if no tasks have status "review",
|
||||
* it does nothing.
|
||||
*/
|
||||
|
|
@ -34,8 +36,7 @@ export class MigrationService {
|
|||
|
||||
// Cast to string for comparison since "review" is no longer in TaskStatus type
|
||||
// but may exist in legacy data
|
||||
const isReviewStatus = (status: TaskStatus): boolean =>
|
||||
(status as string) === 'review';
|
||||
const isReviewStatus = (status: TaskStatus): boolean => (status as string) === 'review';
|
||||
|
||||
// Migrate active tasks
|
||||
const activeTasks = await this.taskService.listTasks();
|
||||
|
|
@ -58,7 +59,7 @@ export class MigrationService {
|
|||
}
|
||||
|
||||
if (migratedCount > 0) {
|
||||
console.log(`Migrated ${migratedCount} tasks from review → blocked`);
|
||||
log.info(`Migrated ${migratedCount} tasks from review → blocked`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,10 +75,10 @@ export class MigrationService {
|
|||
|
||||
// Temporarily restore
|
||||
await this.taskService.restoreTask(taskId);
|
||||
|
||||
|
||||
// Update status (restoreTask sets status to 'done', so we need to correct it)
|
||||
await this.taskService.updateTask(taskId, { status: 'blocked' });
|
||||
|
||||
|
||||
// Re-archive
|
||||
await this.taskService.archiveTask(taskId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { resolve } from 'path';
|
|||
import type { ProjectConfig } from '@veritas-kanban/shared';
|
||||
import { ManagedListService } from './managed-list-service.js';
|
||||
import { TaskService } from './task-service.js';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('project-service');
|
||||
|
||||
// Color palette for auto-seeded projects
|
||||
const PROJECT_COLORS = [
|
||||
|
|
@ -23,7 +25,7 @@ export class ProjectService extends ManagedListService<ProjectConfig> {
|
|||
|
||||
constructor(taskService: TaskService) {
|
||||
const configDir = resolve(process.cwd(), '..', '.veritas-kanban');
|
||||
|
||||
|
||||
super({
|
||||
filename: 'projects.json',
|
||||
configDir,
|
||||
|
|
@ -59,7 +61,7 @@ export class ProjectService extends ManagedListService<ProjectConfig> {
|
|||
private async seedProjectsFromTasks(): Promise<void> {
|
||||
const configDir = resolve(process.cwd(), '..', '.veritas-kanban');
|
||||
const projectsFile = resolve(configDir, 'projects.json');
|
||||
|
||||
|
||||
// Only seed if the file is empty or has no items
|
||||
const existingProjects = await this.list(true);
|
||||
if (existingProjects.length > 0) {
|
||||
|
|
@ -75,7 +77,7 @@ export class ProjectService extends ManagedListService<ProjectConfig> {
|
|||
const allTasks = [...activeTasks, ...archivedTasks];
|
||||
const projectStrings = new Set<string>();
|
||||
|
||||
allTasks.forEach(task => {
|
||||
allTasks.forEach((task) => {
|
||||
if (task.project) {
|
||||
projectStrings.add(task.project);
|
||||
}
|
||||
|
|
@ -86,13 +88,13 @@ export class ProjectService extends ManagedListService<ProjectConfig> {
|
|||
// (tasks store project as a plain string that must match the project ID)
|
||||
const projectArray = Array.from(projectStrings).sort();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
|
||||
for (let i = 0; i < projectArray.length; i++) {
|
||||
const projectName = projectArray[i];
|
||||
const color = PROJECT_COLORS[i % PROJECT_COLORS.length];
|
||||
|
||||
|
||||
await this.seedItem({
|
||||
id: projectName, // Must match existing task.project values
|
||||
id: projectName, // Must match existing task.project values
|
||||
label: projectName,
|
||||
color,
|
||||
order: i,
|
||||
|
|
@ -101,6 +103,6 @@ export class ProjectService extends ManagedListService<ProjectConfig> {
|
|||
} as ProjectConfig);
|
||||
}
|
||||
|
||||
console.log(`✅ Seeded ${projectArray.length} projects from existing tasks`);
|
||||
log.info(`✅ Seeded ${projectArray.length} projects from existing tasks`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { resolve } from 'path';
|
|||
import type { SprintConfig } from '@veritas-kanban/shared';
|
||||
import { ManagedListService } from './managed-list-service.js';
|
||||
import { TaskService } from './task-service.js';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('sprint-service');
|
||||
|
||||
export class SprintService extends ManagedListService<SprintConfig> {
|
||||
private taskService: TaskService;
|
||||
|
|
@ -9,7 +11,7 @@ export class SprintService extends ManagedListService<SprintConfig> {
|
|||
|
||||
constructor(taskService: TaskService) {
|
||||
const configDir = resolve(process.cwd(), '..', '.veritas-kanban');
|
||||
|
||||
|
||||
super({
|
||||
filename: 'sprints.json',
|
||||
configDir,
|
||||
|
|
@ -45,7 +47,7 @@ export class SprintService extends ManagedListService<SprintConfig> {
|
|||
private async seedSprintsFromTasks(): Promise<void> {
|
||||
const configDir = resolve(process.cwd(), '..', '.veritas-kanban');
|
||||
const sprintsFile = resolve(configDir, 'sprints.json');
|
||||
|
||||
|
||||
// Only seed if the file is empty or has no items
|
||||
const existingSprints = await this.list(true);
|
||||
if (existingSprints.length > 0) {
|
||||
|
|
@ -61,7 +63,7 @@ export class SprintService extends ManagedListService<SprintConfig> {
|
|||
const allTasks = [...activeTasks, ...archivedTasks];
|
||||
const sprintStrings = new Set<string>();
|
||||
|
||||
allTasks.forEach(task => {
|
||||
allTasks.forEach((task) => {
|
||||
if (task.sprint) {
|
||||
sprintStrings.add(task.sprint);
|
||||
}
|
||||
|
|
@ -72,12 +74,12 @@ export class SprintService extends ManagedListService<SprintConfig> {
|
|||
// (tasks store sprint as a plain string that must match the sprint ID)
|
||||
const sprintArray = Array.from(sprintStrings).sort();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
|
||||
for (let i = 0; i < sprintArray.length; i++) {
|
||||
const sprintName = sprintArray[i];
|
||||
|
||||
|
||||
await this.seedItem({
|
||||
id: sprintName, // Must match existing task.sprint values
|
||||
id: sprintName, // Must match existing task.sprint values
|
||||
label: sprintName,
|
||||
order: i,
|
||||
created: now,
|
||||
|
|
@ -85,6 +87,6 @@ export class SprintService extends ManagedListService<SprintConfig> {
|
|||
} as SprintConfig);
|
||||
}
|
||||
|
||||
console.log(`✅ Seeded ${sprintArray.length} sprints from existing tasks`);
|
||||
log.info(`✅ Seeded ${sprintArray.length} sprints from existing tasks`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { readFile, writeFile, mkdir } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('status-history-service');
|
||||
|
||||
export type AgentStatusState = 'idle' | 'working' | 'thinking' | 'sub-agent' | 'error';
|
||||
|
||||
|
|
@ -129,7 +131,7 @@ export class StatusHistoryService {
|
|||
|
||||
this.lastEntry = entry;
|
||||
|
||||
console.log(
|
||||
log.info(
|
||||
`[StatusHistory] ${previousStatus} → ${newStatus}${taskId ? ` (task: ${taskId})` : ''}`
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import type {
|
|||
TelemetryQueryOptions,
|
||||
AnyTelemetryEvent,
|
||||
} from '@veritas-kanban/shared';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('telemetry-service');
|
||||
|
||||
// Default paths - resolve to project root
|
||||
const PROJECT_ROOT = path.resolve(process.cwd(), '..');
|
||||
|
|
@ -125,9 +127,9 @@ export class TelemetryService {
|
|||
this.pendingWrites.push(fullEvent);
|
||||
if (this.pendingWrites.length > this.MAX_QUEUE_SIZE) {
|
||||
const dropped = this.pendingWrites.shift();
|
||||
console.warn(
|
||||
`[Telemetry] Queue size exceeded (${this.MAX_QUEUE_SIZE}), dropped event:`,
|
||||
dropped?.type
|
||||
log.warn(
|
||||
{ droppedType: dropped?.type },
|
||||
`[Telemetry] Queue size exceeded (${this.MAX_QUEUE_SIZE}), dropped event`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +142,7 @@ export class TelemetryService {
|
|||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('[Telemetry] Failed to write event:', err);
|
||||
log.error({ err: err }, '[Telemetry] Failed to write event');
|
||||
});
|
||||
|
||||
this.writeQueue = writePromise;
|
||||
|
|
@ -407,7 +409,7 @@ export class TelemetryService {
|
|||
try {
|
||||
return JSON.parse(line) as AnyTelemetryEvent;
|
||||
} catch {
|
||||
console.error('[Telemetry] Failed to parse line:', line);
|
||||
log.error({ err: line }, '[Telemetry] Failed to parse line');
|
||||
return null;
|
||||
}
|
||||
})
|
||||
|
|
@ -497,13 +499,13 @@ export class TelemetryService {
|
|||
await this.compressFile(filepath);
|
||||
compressed++;
|
||||
} catch (err) {
|
||||
console.error(`[Telemetry] Failed to compress ${filename}:`, err);
|
||||
log.error({ err: err }, `[Telemetry] Failed to compress ${filename}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deleted > 0 || compressed > 0) {
|
||||
console.log(
|
||||
log.info(
|
||||
`[Telemetry] Cleanup: deleted ${deleted} expired file(s), compressed ${compressed} file(s) ` +
|
||||
`(retention=${this.config.retention}d, compress=${this.compressAfterDays}d)`
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@ import { readdir, readFile, writeFile, unlink, mkdir } from 'fs/promises';
|
|||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import type { TaskTemplate, CreateTemplateInput, UpdateTemplateInput } from '@veritas-kanban/shared';
|
||||
import type {
|
||||
TaskTemplate,
|
||||
CreateTemplateInput,
|
||||
UpdateTemplateInput,
|
||||
} from '@veritas-kanban/shared';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('template-service');
|
||||
|
||||
export class TemplateService {
|
||||
private templatesDir: string;
|
||||
|
|
@ -66,20 +72,20 @@ export class TemplateService {
|
|||
|
||||
async getTemplates(): Promise<TaskTemplate[]> {
|
||||
await this.ensureDir();
|
||||
|
||||
|
||||
const files = await readdir(this.templatesDir);
|
||||
const templates: TaskTemplate[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.md')) continue;
|
||||
|
||||
|
||||
try {
|
||||
const content = await readFile(join(this.templatesDir, file), 'utf-8');
|
||||
const { data } = matter(content);
|
||||
const migrated = this.migrateTemplate(data);
|
||||
templates.push(migrated);
|
||||
} catch (err) {
|
||||
console.error(`Error reading template ${file}:`, err);
|
||||
log.error({ err: err }, `Error reading template ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +94,7 @@ export class TemplateService {
|
|||
|
||||
async getTemplate(id: string): Promise<TaskTemplate | null> {
|
||||
const path = this.templatePath(id);
|
||||
|
||||
|
||||
if (!existsSync(path)) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -98,17 +104,17 @@ export class TemplateService {
|
|||
const { data } = matter(content);
|
||||
return this.migrateTemplate(data);
|
||||
} catch (err) {
|
||||
console.error(`Error reading template ${id}:`, err);
|
||||
log.error({ err: err }, `Error reading template ${id}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async createTemplate(input: CreateTemplateInput): Promise<TaskTemplate> {
|
||||
await this.ensureDir();
|
||||
|
||||
|
||||
const id = `template_${this.slugify(input.name)}_${Date.now()}`;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
|
||||
const template: TaskTemplate = {
|
||||
id,
|
||||
name: input.name,
|
||||
|
|
@ -124,7 +130,7 @@ export class TemplateService {
|
|||
|
||||
const content = matter.stringify('', template);
|
||||
await writeFile(this.templatePath(id), content, 'utf-8');
|
||||
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
|
|
@ -149,13 +155,13 @@ export class TemplateService {
|
|||
|
||||
const content = matter.stringify('', updated);
|
||||
await writeFile(this.templatePath(id), content, 'utf-8');
|
||||
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async deleteTemplate(id: string): Promise<boolean> {
|
||||
const path = this.templatePath(id);
|
||||
|
||||
|
||||
if (!existsSync(path)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import path from 'path';
|
|||
import { extractText as unpdfExtract } from 'unpdf';
|
||||
import mammoth from 'mammoth';
|
||||
import ExcelJS from 'exceljs';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('text-extraction-service');
|
||||
|
||||
export interface TextExtractionResult {
|
||||
text: string | null;
|
||||
|
|
@ -59,8 +61,12 @@ export class TextExtractionService {
|
|||
}
|
||||
|
||||
// XML/YAML
|
||||
if (mimeType === 'application/xml' || mimeType === 'text/xml' ||
|
||||
mimeType === 'application/yaml' || mimeType === 'text/yaml') {
|
||||
if (
|
||||
mimeType === 'application/xml' ||
|
||||
mimeType === 'text/xml' ||
|
||||
mimeType === 'application/yaml' ||
|
||||
mimeType === 'text/yaml'
|
||||
) {
|
||||
return await this.extractPlainText(filepath);
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +78,7 @@ export class TextExtractionService {
|
|||
// Unknown type
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`Text extraction failed for ${filepath}:`, error);
|
||||
log.error({ err: error }, `Text extraction failed for ${filepath}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -94,7 +100,7 @@ export class TextExtractionService {
|
|||
const { text } = await unpdfExtract(buffer, { mergePages: true });
|
||||
return text || null;
|
||||
} catch (error) {
|
||||
console.error('PDF extraction error:', error);
|
||||
log.error({ err: error }, 'PDF extraction error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +114,7 @@ export class TextExtractionService {
|
|||
const result = await mammoth.extractRawText({ buffer });
|
||||
return result.value || null;
|
||||
} catch (error) {
|
||||
console.error('DOCX extraction error:', error);
|
||||
log.error({ err: error }, 'DOCX extraction error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -120,23 +126,23 @@ export class TextExtractionService {
|
|||
private async extractXLSX(filepath: string): Promise<string | null> {
|
||||
try {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
|
||||
|
||||
// Read the workbook directly from file
|
||||
await workbook.xlsx.readFile(filepath);
|
||||
|
||||
|
||||
// Extract all sheets
|
||||
const sheets: string[] = [];
|
||||
|
||||
|
||||
workbook.eachSheet((worksheet, sheetId) => {
|
||||
const rows: string[] = [];
|
||||
|
||||
|
||||
worksheet.eachRow((row, rowNumber) => {
|
||||
const values: string[] = [];
|
||||
|
||||
|
||||
row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
||||
// Get cell value as string
|
||||
const value = cell.value;
|
||||
|
||||
|
||||
// Handle different value types
|
||||
if (value === null || value === undefined) {
|
||||
values.push('');
|
||||
|
|
@ -150,18 +156,18 @@ export class TextExtractionService {
|
|||
values.push(value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
rows.push(values.join(','));
|
||||
});
|
||||
|
||||
|
||||
if (rows.length > 0) {
|
||||
sheets.push(`=== Sheet: ${worksheet.name} ===\n${rows.join('\n')}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return sheets.length > 0 ? sheets.join('\n\n') : null;
|
||||
} catch (error) {
|
||||
console.error('XLSX extraction error:', error);
|
||||
log.error({ err: error }, 'XLSX extraction error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -172,7 +178,7 @@ export class TextExtractionService {
|
|||
private async extractHTML(filepath: string): Promise<string | null> {
|
||||
try {
|
||||
const html = await fs.readFile(filepath, 'utf-8');
|
||||
|
||||
|
||||
// Simple tag stripping (for more complex HTML, consider using a library like cheerio)
|
||||
const text = html
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||
|
|
@ -180,10 +186,10 @@ export class TextExtractionService {
|
|||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
|
||||
return text || null;
|
||||
} catch (error) {
|
||||
console.error('HTML extraction error:', error);
|
||||
log.error({ err: error }, 'HTML extraction error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -197,7 +203,7 @@ export class TextExtractionService {
|
|||
const json = JSON.parse(content);
|
||||
return JSON.stringify(json, null, 2);
|
||||
} catch (error) {
|
||||
console.error('JSON extraction error:', error);
|
||||
log.error({ err: error }, 'JSON extraction error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { TaskService } from './task-service.js';
|
|||
import type { Task } from '@veritas-kanban/shared';
|
||||
import { spawn } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
const log = createLogger('worktree-service');
|
||||
|
||||
// Default paths
|
||||
const PROJECT_ROOT = path.resolve(process.cwd(), '..');
|
||||
|
|
@ -151,7 +153,7 @@ export class WorktreeService {
|
|||
await this.execGitWithTimeout(repoPath, ['fetch']);
|
||||
} catch (e: any) {
|
||||
// Ignore fetch errors (might be offline)
|
||||
console.warn('Could not fetch from remote:', e.message);
|
||||
log.warn('Could not fetch from remote:', e.message);
|
||||
}
|
||||
|
||||
// Check if branch already exists
|
||||
|
|
@ -215,7 +217,7 @@ export class WorktreeService {
|
|||
const [behind, ahead] = log.trim().split('\t').map(Number);
|
||||
aheadBehind = { ahead: ahead || 0, behind: behind || 0 };
|
||||
} catch (e: any) {
|
||||
console.warn('Could not get ahead/behind info:', e.message);
|
||||
log.warn('Could not get ahead/behind info:', e.message);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue