@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
288 lines (287 loc) • 8.33 kB
JavaScript
import { fileURLToPath as __fileURLToPath } from 'url';
import { dirname as __pathDirname } from 'path';
const __filename = __fileURLToPath(import.meta.url);
const __dirname = __pathDirname(__filename);
import express from "express";
import { AnalyticsService } from "../core/analytics-service.js";
import { WebSocketServer, WebSocket } from "ws";
class AnalyticsAPI {
router;
analyticsService;
wss;
constructor(projectPath) {
this.router = express.Router();
this.analyticsService = new AnalyticsService(projectPath);
this.setupRoutes();
}
setupRoutes() {
this.router.use(express.json());
this.router.get("/metrics", this.getMetrics.bind(this));
this.router.get("/tasks", this.getTasks.bind(this));
this.router.get("/team/:userId", this.getTeamMetrics.bind(this));
this.router.post("/tasks", this.addTask.bind(this));
this.router.put("/tasks/:taskId", this.updateTask.bind(this));
this.router.post("/sync", this.syncLinear.bind(this));
this.router.get("/export", this.exportMetrics.bind(this));
}
async getMetrics(req, res) {
try {
const query = this.parseQuery(req.query);
const dashboardState = await this.analyticsService.getDashboardState(query);
res.json({
success: true,
data: {
metrics: dashboardState.metrics,
lastUpdated: dashboardState.lastUpdated
}
});
} catch (error) {
this.handleError(res, error);
}
}
async getTasks(req, res) {
try {
const query = this.parseQuery(req.query);
const dashboardState = await this.analyticsService.getDashboardState(query);
res.json({
success: true,
data: {
tasks: dashboardState.recentTasks,
total: dashboardState.metrics.totalTasks
}
});
} catch (error) {
this.handleError(res, error);
}
}
async getTeamMetrics(req, res) {
try {
const { userId } = req.params;
const query = this.parseQuery(req.query);
if (userId === "all") {
const dashboardState = await this.analyticsService.getDashboardState(query);
res.json({
success: true,
data: dashboardState.teamMetrics
});
} else {
const dashboardState = await this.analyticsService.getDashboardState({
...query,
userIds: [userId]
});
const userMetrics = dashboardState.teamMetrics.find(
(m) => m.userId === userId
);
if (!userMetrics) {
res.status(404).json({
success: false,
error: "User metrics not found"
});
return;
}
res.json({
success: true,
data: userMetrics
});
}
} catch (error) {
this.handleError(res, error);
}
}
async addTask(req, res) {
try {
const task = {
...req.body,
createdAt: new Date(req.body.createdAt || Date.now()),
completedAt: req.body.completedAt ? new Date(req.body.completedAt) : void 0
};
await this.analyticsService.addTask(task);
res.status(201).json({
success: true,
message: "Task added successfully"
});
} catch (error) {
this.handleError(res, error);
}
}
async updateTask(req, res) {
try {
const { taskId } = req.params;
const updates = req.body;
if (updates.completedAt) {
updates.completedAt = new Date(updates.completedAt);
}
await this.analyticsService.updateTask(taskId, updates);
res.json({
success: true,
message: "Task updated successfully"
});
} catch (error) {
this.handleError(res, error);
}
}
async syncLinear(req, res) {
try {
await this.analyticsService.syncLinearTasks();
res.json({
success: true,
message: "Linear tasks synced successfully"
});
} catch (error) {
this.handleError(res, error);
}
}
async exportMetrics(req, res) {
try {
const query = this.parseQuery(req.query);
const format = req.query.format || "json";
const dashboardState = await this.analyticsService.getDashboardState(query);
if (format === "csv") {
const csv = this.convertToCSV(dashboardState);
res.setHeader("Content-Type", "text/csv");
res.setHeader(
"Content-Disposition",
'attachment; filename="analytics-export.csv"'
);
res.send(csv);
} else {
res.json({
success: true,
data: dashboardState
});
}
} catch (error) {
this.handleError(res, error);
}
}
parseQuery(query) {
const result = {};
if (query.start && query.end) {
result.timeRange = {
start: new Date(query.start),
end: new Date(query.end),
preset: query.preset
};
} else if (query.preset) {
result.timeRange = this.getPresetTimeRange(query.preset);
}
if (query.users) {
result.userIds = Array.isArray(query.users) ? query.users : [query.users];
}
if (query.states) {
result.states = Array.isArray(query.states) ? query.states : [query.states];
}
if (query.priorities) {
result.priorities = Array.isArray(query.priorities) ? query.priorities : [query.priorities];
}
if (query.labels) {
result.labels = Array.isArray(query.labels) ? query.labels : [query.labels];
}
if (query.limit) {
result.limit = parseInt(query.limit);
}
if (query.offset) {
result.offset = parseInt(query.offset);
}
return result;
}
getPresetTimeRange(preset) {
const end = /* @__PURE__ */ new Date();
const start = /* @__PURE__ */ new Date();
switch (preset) {
case "today":
start.setHours(0, 0, 0, 0);
break;
case "7d":
start.setDate(start.getDate() - 7);
break;
case "30d":
start.setDate(start.getDate() - 30);
break;
case "90d":
start.setDate(start.getDate() - 90);
break;
default:
start.setDate(start.getDate() - 7);
}
return { start, end, preset };
}
convertToCSV(dashboardState) {
const tasks = dashboardState.recentTasks;
if (!tasks || tasks.length === 0) return "No data";
const headers = Object.keys(tasks[0]).join(",");
const rows = tasks.map(
(task) => Object.values(task).map((v) => typeof v === "object" ? JSON.stringify(v) : v).join(",")
);
return [headers, ...rows].join("\n");
}
handleError(res, error) {
console.error("Analytics API error:", error);
res.status(500).json({
success: false,
error: error.message || "Internal server error"
});
}
setupWebSocket(server) {
this.wss = new WebSocketServer({
server,
path: "/ws/analytics"
});
this.wss.on("connection", (ws) => {
console.log("WebSocket client connected to analytics");
const unsubscribe = this.analyticsService.subscribeToUpdates((state) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(
JSON.stringify({
type: "update",
data: state
})
);
}
});
ws.on("message", async (message) => {
try {
const data = JSON.parse(message);
if (data.type === "subscribe") {
const query = this.parseQuery(data.query || {});
const state = await this.analyticsService.getDashboardState(query);
ws.send(
JSON.stringify({
type: "initial",
data: state
})
);
}
} catch (error) {
ws.send(
JSON.stringify({
type: "error",
error: "Invalid message format"
})
);
}
});
ws.on("close", () => {
unsubscribe();
console.log("WebSocket client disconnected");
});
ws.on("error", (error) => {
console.error("WebSocket error:", error);
unsubscribe();
});
});
}
getRouter() {
return this.router;
}
close() {
this.analyticsService.close();
if (this.wss) {
this.wss.close();
}
}
}
export {
AnalyticsAPI
};
//# sourceMappingURL=analytics-api.js.map