@hasna/microservices 0.0.10 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +86 -1
- package/bin/mcp.js +86 -1
- package/dist/index.js +86 -1
- package/microservices/microservice-analytics/package.json +27 -0
- package/microservices/microservice-analytics/src/cli/index.ts +373 -0
- package/microservices/microservice-analytics/src/db/analytics.ts +564 -0
- package/microservices/microservice-analytics/src/db/database.ts +93 -0
- package/microservices/microservice-analytics/src/db/migrations.ts +50 -0
- package/microservices/microservice-analytics/src/index.ts +37 -0
- package/microservices/microservice-analytics/src/mcp/index.ts +334 -0
- package/microservices/microservice-assets/package.json +27 -0
- package/microservices/microservice-assets/src/cli/index.ts +375 -0
- package/microservices/microservice-assets/src/db/assets.ts +370 -0
- package/microservices/microservice-assets/src/db/database.ts +93 -0
- package/microservices/microservice-assets/src/db/migrations.ts +51 -0
- package/microservices/microservice-assets/src/index.ts +32 -0
- package/microservices/microservice-assets/src/mcp/index.ts +346 -0
- package/microservices/microservice-compliance/package.json +27 -0
- package/microservices/microservice-compliance/src/cli/index.ts +467 -0
- package/microservices/microservice-compliance/src/db/compliance.ts +633 -0
- package/microservices/microservice-compliance/src/db/database.ts +93 -0
- package/microservices/microservice-compliance/src/db/migrations.ts +63 -0
- package/microservices/microservice-compliance/src/index.ts +46 -0
- package/microservices/microservice-compliance/src/mcp/index.ts +438 -0
- package/microservices/microservice-habits/package.json +27 -0
- package/microservices/microservice-habits/src/cli/index.ts +315 -0
- package/microservices/microservice-habits/src/db/database.ts +93 -0
- package/microservices/microservice-habits/src/db/habits.ts +451 -0
- package/microservices/microservice-habits/src/db/migrations.ts +46 -0
- package/microservices/microservice-habits/src/index.ts +31 -0
- package/microservices/microservice-habits/src/mcp/index.ts +313 -0
- package/microservices/microservice-health/package.json +27 -0
- package/microservices/microservice-health/src/cli/index.ts +484 -0
- package/microservices/microservice-health/src/db/database.ts +93 -0
- package/microservices/microservice-health/src/db/health.ts +708 -0
- package/microservices/microservice-health/src/db/migrations.ts +70 -0
- package/microservices/microservice-health/src/index.ts +63 -0
- package/microservices/microservice-health/src/mcp/index.ts +437 -0
- package/microservices/microservice-notifications/package.json +27 -0
- package/microservices/microservice-notifications/src/cli/index.ts +349 -0
- package/microservices/microservice-notifications/src/db/database.ts +93 -0
- package/microservices/microservice-notifications/src/db/migrations.ts +62 -0
- package/microservices/microservice-notifications/src/db/notifications.ts +509 -0
- package/microservices/microservice-notifications/src/index.ts +41 -0
- package/microservices/microservice-notifications/src/mcp/index.ts +422 -0
- package/microservices/microservice-products/package.json +27 -0
- package/microservices/microservice-products/src/cli/index.ts +416 -0
- package/microservices/microservice-products/src/db/categories.ts +154 -0
- package/microservices/microservice-products/src/db/database.ts +93 -0
- package/microservices/microservice-products/src/db/migrations.ts +58 -0
- package/microservices/microservice-products/src/db/pricing-tiers.ts +66 -0
- package/microservices/microservice-products/src/db/products.ts +452 -0
- package/microservices/microservice-products/src/index.ts +53 -0
- package/microservices/microservice-products/src/mcp/index.ts +453 -0
- package/microservices/microservice-projects/package.json +27 -0
- package/microservices/microservice-projects/src/cli/index.ts +480 -0
- package/microservices/microservice-projects/src/db/database.ts +93 -0
- package/microservices/microservice-projects/src/db/migrations.ts +65 -0
- package/microservices/microservice-projects/src/db/projects.ts +715 -0
- package/microservices/microservice-projects/src/index.ts +57 -0
- package/microservices/microservice-projects/src/mcp/index.ts +501 -0
- package/microservices/microservice-proposals/package.json +27 -0
- package/microservices/microservice-proposals/src/cli/index.ts +400 -0
- package/microservices/microservice-proposals/src/db/database.ts +93 -0
- package/microservices/microservice-proposals/src/db/migrations.ts +52 -0
- package/microservices/microservice-proposals/src/db/proposals.ts +532 -0
- package/microservices/microservice-proposals/src/index.ts +37 -0
- package/microservices/microservice-proposals/src/mcp/index.ts +375 -0
- package/microservices/microservice-reading/package.json +27 -0
- package/microservices/microservice-reading/src/cli/index.ts +464 -0
- package/microservices/microservice-reading/src/db/database.ts +93 -0
- package/microservices/microservice-reading/src/db/migrations.ts +59 -0
- package/microservices/microservice-reading/src/db/reading.ts +524 -0
- package/microservices/microservice-reading/src/index.ts +51 -0
- package/microservices/microservice-reading/src/mcp/index.ts +368 -0
- package/microservices/microservice-travel/package.json +27 -0
- package/microservices/microservice-travel/src/cli/index.ts +505 -0
- package/microservices/microservice-travel/src/db/database.ts +93 -0
- package/microservices/microservice-travel/src/db/migrations.ts +77 -0
- package/microservices/microservice-travel/src/db/travel.ts +802 -0
- package/microservices/microservice-travel/src/index.ts +60 -0
- package/microservices/microservice-travel/src/mcp/index.ts +495 -0
- package/microservices/microservice-wiki/package.json +27 -0
- package/microservices/microservice-wiki/src/cli/index.ts +345 -0
- package/microservices/microservice-wiki/src/db/database.ts +93 -0
- package/microservices/microservice-wiki/src/db/migrations.ts +55 -0
- package/microservices/microservice-wiki/src/db/wiki.ts +395 -0
- package/microservices/microservice-wiki/src/index.ts +32 -0
- package/microservices/microservice-wiki/src/mcp/index.ts +344 -0
- package/package.json +1 -1
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics CRUD operations — KPIs, Dashboards, Reports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getDatabase } from "./database.js";
|
|
6
|
+
|
|
7
|
+
// ─── KPI Types ───
|
|
8
|
+
|
|
9
|
+
export interface Kpi {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
category: string | null;
|
|
13
|
+
value: number;
|
|
14
|
+
period: string | null;
|
|
15
|
+
source_service: string | null;
|
|
16
|
+
metadata: Record<string, unknown>;
|
|
17
|
+
recorded_at: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface KpiRow {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
category: string | null;
|
|
24
|
+
value: number;
|
|
25
|
+
period: string | null;
|
|
26
|
+
source_service: string | null;
|
|
27
|
+
metadata: string;
|
|
28
|
+
recorded_at: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function rowToKpi(row: KpiRow): Kpi {
|
|
32
|
+
return {
|
|
33
|
+
...row,
|
|
34
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Dashboard Types ───
|
|
39
|
+
|
|
40
|
+
export interface Dashboard {
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
description: string | null;
|
|
44
|
+
widgets: unknown[];
|
|
45
|
+
created_at: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface DashboardRow {
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
description: string | null;
|
|
52
|
+
widgets: string;
|
|
53
|
+
created_at: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function rowToDashboard(row: DashboardRow): Dashboard {
|
|
57
|
+
return {
|
|
58
|
+
...row,
|
|
59
|
+
widgets: JSON.parse(row.widgets || "[]"),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Report Types ───
|
|
64
|
+
|
|
65
|
+
export type ReportType = "daily" | "weekly" | "monthly" | "quarterly" | "annual" | "custom";
|
|
66
|
+
|
|
67
|
+
export interface Report {
|
|
68
|
+
id: string;
|
|
69
|
+
name: string;
|
|
70
|
+
type: ReportType;
|
|
71
|
+
content: string | null;
|
|
72
|
+
period: string | null;
|
|
73
|
+
generated_at: string;
|
|
74
|
+
metadata: Record<string, unknown>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface ReportRow {
|
|
78
|
+
id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
type: ReportType;
|
|
81
|
+
content: string | null;
|
|
82
|
+
period: string | null;
|
|
83
|
+
generated_at: string;
|
|
84
|
+
metadata: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function rowToReport(row: ReportRow): Report {
|
|
88
|
+
return {
|
|
89
|
+
...row,
|
|
90
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── KPI Operations ───
|
|
95
|
+
|
|
96
|
+
export interface RecordKpiInput {
|
|
97
|
+
name: string;
|
|
98
|
+
value: number;
|
|
99
|
+
category?: string;
|
|
100
|
+
source_service?: string;
|
|
101
|
+
period?: string;
|
|
102
|
+
metadata?: Record<string, unknown>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function recordKpi(input: RecordKpiInput): Kpi {
|
|
106
|
+
const db = getDatabase();
|
|
107
|
+
const id = crypto.randomUUID();
|
|
108
|
+
const metadata = JSON.stringify(input.metadata || {});
|
|
109
|
+
|
|
110
|
+
db.prepare(
|
|
111
|
+
`INSERT INTO kpis (id, name, value, category, source_service, period, metadata)
|
|
112
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
113
|
+
).run(
|
|
114
|
+
id,
|
|
115
|
+
input.name,
|
|
116
|
+
input.value,
|
|
117
|
+
input.category || null,
|
|
118
|
+
input.source_service || null,
|
|
119
|
+
input.period || null,
|
|
120
|
+
metadata
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return getKpiById(id)!;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getKpiById(id: string): Kpi | null {
|
|
127
|
+
const db = getDatabase();
|
|
128
|
+
const row = db.prepare("SELECT * FROM kpis WHERE id = ?").get(id) as KpiRow | null;
|
|
129
|
+
return row ? rowToKpi(row) : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getKpi(name: string, period?: string): Kpi | null {
|
|
133
|
+
const db = getDatabase();
|
|
134
|
+
let sql = "SELECT * FROM kpis WHERE name = ?";
|
|
135
|
+
const params: unknown[] = [name];
|
|
136
|
+
|
|
137
|
+
if (period) {
|
|
138
|
+
sql += " AND period = ?";
|
|
139
|
+
params.push(period);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
sql += " ORDER BY recorded_at DESC LIMIT 1";
|
|
143
|
+
|
|
144
|
+
const row = db.prepare(sql).get(...params) as KpiRow | null;
|
|
145
|
+
return row ? rowToKpi(row) : null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getKpiTrend(name: string, days: number = 30): Kpi[] {
|
|
149
|
+
const db = getDatabase();
|
|
150
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
151
|
+
|
|
152
|
+
const rows = db
|
|
153
|
+
.prepare(
|
|
154
|
+
"SELECT * FROM kpis WHERE name = ? AND recorded_at >= ? ORDER BY recorded_at ASC"
|
|
155
|
+
)
|
|
156
|
+
.all(name, cutoff) as KpiRow[];
|
|
157
|
+
|
|
158
|
+
return rows.map(rowToKpi);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface ListKpisOptions {
|
|
162
|
+
category?: string;
|
|
163
|
+
source_service?: string;
|
|
164
|
+
limit?: number;
|
|
165
|
+
offset?: number;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function listKpis(options: ListKpisOptions = {}): Kpi[] {
|
|
169
|
+
const db = getDatabase();
|
|
170
|
+
const conditions: string[] = [];
|
|
171
|
+
const params: unknown[] = [];
|
|
172
|
+
|
|
173
|
+
if (options.category) {
|
|
174
|
+
conditions.push("category = ?");
|
|
175
|
+
params.push(options.category);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (options.source_service) {
|
|
179
|
+
conditions.push("source_service = ?");
|
|
180
|
+
params.push(options.source_service);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let sql = "SELECT * FROM kpis";
|
|
184
|
+
if (conditions.length > 0) {
|
|
185
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
186
|
+
}
|
|
187
|
+
sql += " ORDER BY recorded_at DESC";
|
|
188
|
+
|
|
189
|
+
if (options.limit) {
|
|
190
|
+
sql += " LIMIT ?";
|
|
191
|
+
params.push(options.limit);
|
|
192
|
+
}
|
|
193
|
+
if (options.offset) {
|
|
194
|
+
sql += " OFFSET ?";
|
|
195
|
+
params.push(options.offset);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const rows = db.prepare(sql).all(...params) as KpiRow[];
|
|
199
|
+
return rows.map(rowToKpi);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function getLatestKpis(): Kpi[] {
|
|
203
|
+
const db = getDatabase();
|
|
204
|
+
// Get the most recent value for each unique KPI name (using MAX(rowid) to break ties)
|
|
205
|
+
const rows = db
|
|
206
|
+
.prepare(
|
|
207
|
+
`SELECT * FROM kpis WHERE rowid IN (
|
|
208
|
+
SELECT MAX(rowid) FROM kpis GROUP BY name
|
|
209
|
+
) ORDER BY name`
|
|
210
|
+
)
|
|
211
|
+
.all() as KpiRow[];
|
|
212
|
+
|
|
213
|
+
return rows.map(rowToKpi);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function deleteKpi(id: string): boolean {
|
|
217
|
+
const db = getDatabase();
|
|
218
|
+
const result = db.prepare("DELETE FROM kpis WHERE id = ?").run(id);
|
|
219
|
+
return result.changes > 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── Dashboard Operations ───
|
|
223
|
+
|
|
224
|
+
export interface CreateDashboardInput {
|
|
225
|
+
name: string;
|
|
226
|
+
description?: string;
|
|
227
|
+
widgets?: unknown[];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function createDashboard(input: CreateDashboardInput): Dashboard {
|
|
231
|
+
const db = getDatabase();
|
|
232
|
+
const id = crypto.randomUUID();
|
|
233
|
+
const widgets = JSON.stringify(input.widgets || []);
|
|
234
|
+
|
|
235
|
+
db.prepare(
|
|
236
|
+
`INSERT INTO dashboards (id, name, description, widgets)
|
|
237
|
+
VALUES (?, ?, ?, ?)`
|
|
238
|
+
).run(id, input.name, input.description || null, widgets);
|
|
239
|
+
|
|
240
|
+
return getDashboard(id)!;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function getDashboard(id: string): Dashboard | null {
|
|
244
|
+
const db = getDatabase();
|
|
245
|
+
const row = db.prepare("SELECT * FROM dashboards WHERE id = ?").get(id) as DashboardRow | null;
|
|
246
|
+
return row ? rowToDashboard(row) : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function listDashboards(): Dashboard[] {
|
|
250
|
+
const db = getDatabase();
|
|
251
|
+
const rows = db
|
|
252
|
+
.prepare("SELECT * FROM dashboards ORDER BY created_at DESC")
|
|
253
|
+
.all() as DashboardRow[];
|
|
254
|
+
return rows.map(rowToDashboard);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface UpdateDashboardInput {
|
|
258
|
+
name?: string;
|
|
259
|
+
description?: string;
|
|
260
|
+
widgets?: unknown[];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function updateDashboard(
|
|
264
|
+
id: string,
|
|
265
|
+
input: UpdateDashboardInput
|
|
266
|
+
): Dashboard | null {
|
|
267
|
+
const db = getDatabase();
|
|
268
|
+
const existing = getDashboard(id);
|
|
269
|
+
if (!existing) return null;
|
|
270
|
+
|
|
271
|
+
const sets: string[] = [];
|
|
272
|
+
const params: unknown[] = [];
|
|
273
|
+
|
|
274
|
+
if (input.name !== undefined) {
|
|
275
|
+
sets.push("name = ?");
|
|
276
|
+
params.push(input.name);
|
|
277
|
+
}
|
|
278
|
+
if (input.description !== undefined) {
|
|
279
|
+
sets.push("description = ?");
|
|
280
|
+
params.push(input.description);
|
|
281
|
+
}
|
|
282
|
+
if (input.widgets !== undefined) {
|
|
283
|
+
sets.push("widgets = ?");
|
|
284
|
+
params.push(JSON.stringify(input.widgets));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (sets.length === 0) return existing;
|
|
288
|
+
|
|
289
|
+
params.push(id);
|
|
290
|
+
|
|
291
|
+
db.prepare(
|
|
292
|
+
`UPDATE dashboards SET ${sets.join(", ")} WHERE id = ?`
|
|
293
|
+
).run(...params);
|
|
294
|
+
|
|
295
|
+
return getDashboard(id);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function deleteDashboard(id: string): boolean {
|
|
299
|
+
const db = getDatabase();
|
|
300
|
+
const result = db.prepare("DELETE FROM dashboards WHERE id = ?").run(id);
|
|
301
|
+
return result.changes > 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ─── Report Operations ───
|
|
305
|
+
|
|
306
|
+
export interface GenerateReportInput {
|
|
307
|
+
name: string;
|
|
308
|
+
type: ReportType;
|
|
309
|
+
period?: string;
|
|
310
|
+
metadata?: Record<string, unknown>;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function generateReport(input: GenerateReportInput): Report {
|
|
314
|
+
const db = getDatabase();
|
|
315
|
+
const id = crypto.randomUUID();
|
|
316
|
+
const metadata = JSON.stringify(input.metadata || {});
|
|
317
|
+
|
|
318
|
+
// Build report content from current KPIs
|
|
319
|
+
const latestKpis = getLatestKpis();
|
|
320
|
+
const categories = new Map<string, Kpi[]>();
|
|
321
|
+
for (const kpi of latestKpis) {
|
|
322
|
+
const cat = kpi.category || "Uncategorized";
|
|
323
|
+
if (!categories.has(cat)) categories.set(cat, []);
|
|
324
|
+
categories.get(cat)!.push(kpi);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const lines: string[] = [];
|
|
328
|
+
lines.push(`=== ${input.type.toUpperCase()} REPORT: ${input.name} ===`);
|
|
329
|
+
if (input.period) lines.push(`Period: ${input.period}`);
|
|
330
|
+
lines.push(`Generated: ${new Date().toISOString()}`);
|
|
331
|
+
lines.push("");
|
|
332
|
+
|
|
333
|
+
for (const [category, kpis] of categories) {
|
|
334
|
+
lines.push(`--- ${category} ---`);
|
|
335
|
+
for (const kpi of kpis) {
|
|
336
|
+
lines.push(` ${kpi.name}: ${kpi.value}${kpi.period ? ` (${kpi.period})` : ""}`);
|
|
337
|
+
}
|
|
338
|
+
lines.push("");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (latestKpis.length === 0) {
|
|
342
|
+
lines.push("No KPIs recorded yet.");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const content = lines.join("\n");
|
|
346
|
+
|
|
347
|
+
db.prepare(
|
|
348
|
+
`INSERT INTO reports (id, name, type, content, period, metadata)
|
|
349
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
350
|
+
).run(id, input.name, input.type, content, input.period || null, metadata);
|
|
351
|
+
|
|
352
|
+
return getReport(id)!;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function getReport(id: string): Report | null {
|
|
356
|
+
const db = getDatabase();
|
|
357
|
+
const row = db.prepare("SELECT * FROM reports WHERE id = ?").get(id) as ReportRow | null;
|
|
358
|
+
return row ? rowToReport(row) : null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export interface ListReportsOptions {
|
|
362
|
+
type?: ReportType;
|
|
363
|
+
limit?: number;
|
|
364
|
+
offset?: number;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function listReports(options: ListReportsOptions = {}): Report[] {
|
|
368
|
+
const db = getDatabase();
|
|
369
|
+
const conditions: string[] = [];
|
|
370
|
+
const params: unknown[] = [];
|
|
371
|
+
|
|
372
|
+
if (options.type) {
|
|
373
|
+
conditions.push("type = ?");
|
|
374
|
+
params.push(options.type);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let sql = "SELECT * FROM reports";
|
|
378
|
+
if (conditions.length > 0) {
|
|
379
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
380
|
+
}
|
|
381
|
+
sql += " ORDER BY generated_at DESC";
|
|
382
|
+
|
|
383
|
+
if (options.limit) {
|
|
384
|
+
sql += " LIMIT ?";
|
|
385
|
+
params.push(options.limit);
|
|
386
|
+
}
|
|
387
|
+
if (options.offset) {
|
|
388
|
+
sql += " OFFSET ?";
|
|
389
|
+
params.push(options.offset);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const rows = db.prepare(sql).all(...params) as ReportRow[];
|
|
393
|
+
return rows.map(rowToReport);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function deleteReport(id: string): boolean {
|
|
397
|
+
const db = getDatabase();
|
|
398
|
+
const result = db.prepare("DELETE FROM reports WHERE id = ?").run(id);
|
|
399
|
+
return result.changes > 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ─── Business Health ───
|
|
403
|
+
|
|
404
|
+
export interface BusinessHealth {
|
|
405
|
+
total_kpis: number;
|
|
406
|
+
categories: { category: string; count: number; latest_value: number }[];
|
|
407
|
+
latest_kpis: Kpi[];
|
|
408
|
+
report_count: number;
|
|
409
|
+
dashboard_count: number;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function getBusinessHealth(): BusinessHealth {
|
|
413
|
+
const db = getDatabase();
|
|
414
|
+
|
|
415
|
+
const totalKpis = (
|
|
416
|
+
db.prepare("SELECT COUNT(DISTINCT name) as count FROM kpis").get() as { count: number }
|
|
417
|
+
).count;
|
|
418
|
+
|
|
419
|
+
const categoryRows = db
|
|
420
|
+
.prepare(
|
|
421
|
+
`SELECT category, COUNT(DISTINCT name) as count
|
|
422
|
+
FROM kpis WHERE category IS NOT NULL
|
|
423
|
+
GROUP BY category ORDER BY category`
|
|
424
|
+
)
|
|
425
|
+
.all() as { category: string; count: number }[];
|
|
426
|
+
|
|
427
|
+
// Get latest value per category
|
|
428
|
+
const categories = categoryRows.map((row) => {
|
|
429
|
+
const latestInCat = db
|
|
430
|
+
.prepare(
|
|
431
|
+
`SELECT value FROM kpis WHERE category = ? ORDER BY recorded_at DESC LIMIT 1`
|
|
432
|
+
)
|
|
433
|
+
.get(row.category) as { value: number } | null;
|
|
434
|
+
return {
|
|
435
|
+
category: row.category,
|
|
436
|
+
count: row.count,
|
|
437
|
+
latest_value: latestInCat?.value ?? 0,
|
|
438
|
+
};
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const latestKpis = getLatestKpis();
|
|
442
|
+
|
|
443
|
+
const reportCount = (
|
|
444
|
+
db.prepare("SELECT COUNT(*) as count FROM reports").get() as { count: number }
|
|
445
|
+
).count;
|
|
446
|
+
|
|
447
|
+
const dashboardCount = (
|
|
448
|
+
db.prepare("SELECT COUNT(*) as count FROM dashboards").get() as { count: number }
|
|
449
|
+
).count;
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
total_kpis: totalKpis,
|
|
453
|
+
categories,
|
|
454
|
+
latest_kpis: latestKpis,
|
|
455
|
+
report_count: reportCount,
|
|
456
|
+
dashboard_count: dashboardCount,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ─── AI Executive Summary ───
|
|
461
|
+
|
|
462
|
+
export async function generateExecutiveSummary(): Promise<string> {
|
|
463
|
+
const health = getBusinessHealth();
|
|
464
|
+
const latestKpis = getLatestKpis();
|
|
465
|
+
|
|
466
|
+
// Build context for AI
|
|
467
|
+
const kpiSummary = latestKpis
|
|
468
|
+
.map((k) => `${k.name} (${k.category || "uncategorized"}): ${k.value}`)
|
|
469
|
+
.join("\n");
|
|
470
|
+
|
|
471
|
+
const prompt = `You are a business analyst. Generate a concise executive summary based on these KPIs:
|
|
472
|
+
|
|
473
|
+
${kpiSummary || "No KPIs recorded yet."}
|
|
474
|
+
|
|
475
|
+
Total unique KPIs: ${health.total_kpis}
|
|
476
|
+
Categories: ${health.categories.map((c) => `${c.category} (${c.count} KPIs)`).join(", ") || "none"}
|
|
477
|
+
Reports generated: ${health.report_count}
|
|
478
|
+
Dashboards: ${health.dashboard_count}
|
|
479
|
+
|
|
480
|
+
Provide a brief, actionable executive summary in 3-5 sentences.`;
|
|
481
|
+
|
|
482
|
+
// Try Anthropic first, then OpenAI, then fallback
|
|
483
|
+
const anthropicKey = process.env["ANTHROPIC_API_KEY"];
|
|
484
|
+
const openaiKey = process.env["OPENAI_API_KEY"];
|
|
485
|
+
|
|
486
|
+
if (anthropicKey) {
|
|
487
|
+
try {
|
|
488
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
489
|
+
method: "POST",
|
|
490
|
+
headers: {
|
|
491
|
+
"Content-Type": "application/json",
|
|
492
|
+
"x-api-key": anthropicKey,
|
|
493
|
+
"anthropic-version": "2023-06-01",
|
|
494
|
+
},
|
|
495
|
+
body: JSON.stringify({
|
|
496
|
+
model: "claude-sonnet-4-20250514",
|
|
497
|
+
max_tokens: 500,
|
|
498
|
+
messages: [{ role: "user", content: prompt }],
|
|
499
|
+
}),
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
if (response.ok) {
|
|
503
|
+
const data = (await response.json()) as {
|
|
504
|
+
content: { type: string; text: string }[];
|
|
505
|
+
};
|
|
506
|
+
return data.content[0].text;
|
|
507
|
+
}
|
|
508
|
+
} catch {
|
|
509
|
+
// Fall through to OpenAI
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (openaiKey) {
|
|
514
|
+
try {
|
|
515
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
516
|
+
method: "POST",
|
|
517
|
+
headers: {
|
|
518
|
+
"Content-Type": "application/json",
|
|
519
|
+
Authorization: `Bearer ${openaiKey}`,
|
|
520
|
+
},
|
|
521
|
+
body: JSON.stringify({
|
|
522
|
+
model: "gpt-4o-mini",
|
|
523
|
+
max_tokens: 500,
|
|
524
|
+
messages: [{ role: "user", content: prompt }],
|
|
525
|
+
}),
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
if (response.ok) {
|
|
529
|
+
const data = (await response.json()) as {
|
|
530
|
+
choices: { message: { content: string } }[];
|
|
531
|
+
};
|
|
532
|
+
return data.choices[0].message.content;
|
|
533
|
+
}
|
|
534
|
+
} catch {
|
|
535
|
+
// Fall through to local summary
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Local fallback — no AI API keys
|
|
540
|
+
const lines: string[] = [];
|
|
541
|
+
lines.push("=== Executive Summary ===");
|
|
542
|
+
lines.push("");
|
|
543
|
+
|
|
544
|
+
if (latestKpis.length === 0) {
|
|
545
|
+
lines.push("No KPIs have been recorded yet. Start tracking key metrics to enable business health reporting.");
|
|
546
|
+
return lines.join("\n");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
lines.push(`Tracking ${health.total_kpis} unique KPI(s) across ${health.categories.length} category(s).`);
|
|
550
|
+
|
|
551
|
+
for (const cat of health.categories) {
|
|
552
|
+
lines.push(`- ${cat.category}: ${cat.count} KPI(s), latest value: ${cat.latest_value}`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (health.report_count > 0) {
|
|
556
|
+
lines.push(`\n${health.report_count} report(s) have been generated.`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (health.dashboard_count > 0) {
|
|
560
|
+
lines.push(`${health.dashboard_count} dashboard(s) configured.`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return lines.join("\n");
|
|
564
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database connection for microservice-analytics
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Database } from "bun:sqlite";
|
|
6
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { MIGRATIONS } from "./migrations.js";
|
|
9
|
+
|
|
10
|
+
let _db: Database | null = null;
|
|
11
|
+
|
|
12
|
+
function getDbPath(): string {
|
|
13
|
+
// Environment variable override
|
|
14
|
+
if (process.env["MICROSERVICES_DIR"]) {
|
|
15
|
+
return join(process.env["MICROSERVICES_DIR"], "microservice-analytics", "data.db");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check for .microservices in current or parent directories
|
|
19
|
+
let dir = resolve(process.cwd());
|
|
20
|
+
while (true) {
|
|
21
|
+
const candidate = join(dir, ".microservices", "microservice-analytics", "data.db");
|
|
22
|
+
const msDir = join(dir, ".microservices");
|
|
23
|
+
if (existsSync(msDir)) return candidate;
|
|
24
|
+
const parent = dirname(dir);
|
|
25
|
+
if (parent === dir) break;
|
|
26
|
+
dir = parent;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Global fallback
|
|
30
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
31
|
+
return join(home, ".microservices", "microservice-analytics", "data.db");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ensureDir(filePath: string): void {
|
|
35
|
+
const dir = dirname(resolve(filePath));
|
|
36
|
+
if (!existsSync(dir)) {
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getDatabase(): Database {
|
|
42
|
+
if (_db) return _db;
|
|
43
|
+
|
|
44
|
+
const dbPath = getDbPath();
|
|
45
|
+
ensureDir(dbPath);
|
|
46
|
+
|
|
47
|
+
_db = new Database(dbPath);
|
|
48
|
+
_db.exec("PRAGMA journal_mode = WAL");
|
|
49
|
+
_db.exec("PRAGMA foreign_keys = ON");
|
|
50
|
+
|
|
51
|
+
// Create migrations table
|
|
52
|
+
_db.exec(`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
54
|
+
id INTEGER PRIMARY KEY,
|
|
55
|
+
name TEXT NOT NULL,
|
|
56
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
57
|
+
)
|
|
58
|
+
`);
|
|
59
|
+
|
|
60
|
+
// Apply pending migrations
|
|
61
|
+
const applied = _db
|
|
62
|
+
.query("SELECT id FROM _migrations ORDER BY id")
|
|
63
|
+
.all() as { id: number }[];
|
|
64
|
+
const appliedIds = new Set(applied.map((r) => r.id));
|
|
65
|
+
|
|
66
|
+
for (const migration of MIGRATIONS) {
|
|
67
|
+
if (appliedIds.has(migration.id)) continue;
|
|
68
|
+
|
|
69
|
+
_db.exec("BEGIN");
|
|
70
|
+
try {
|
|
71
|
+
_db.exec(migration.sql);
|
|
72
|
+
_db.prepare("INSERT INTO _migrations (id, name) VALUES (?, ?)").run(
|
|
73
|
+
migration.id,
|
|
74
|
+
migration.name
|
|
75
|
+
);
|
|
76
|
+
_db.exec("COMMIT");
|
|
77
|
+
} catch (error) {
|
|
78
|
+
_db.exec("ROLLBACK");
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Migration ${migration.id} (${migration.name}) failed: ${error instanceof Error ? error.message : String(error)}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return _db;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function closeDatabase(): void {
|
|
89
|
+
if (_db) {
|
|
90
|
+
_db.close();
|
|
91
|
+
_db = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface MigrationEntry {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
sql: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const MIGRATIONS: MigrationEntry[] = [
|
|
8
|
+
{
|
|
9
|
+
id: 1,
|
|
10
|
+
name: "initial_schema",
|
|
11
|
+
sql: `
|
|
12
|
+
CREATE TABLE IF NOT EXISTS kpis (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
name TEXT NOT NULL,
|
|
15
|
+
category TEXT,
|
|
16
|
+
value REAL NOT NULL,
|
|
17
|
+
period TEXT,
|
|
18
|
+
source_service TEXT,
|
|
19
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
20
|
+
recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS dashboards (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
name TEXT NOT NULL,
|
|
26
|
+
description TEXT,
|
|
27
|
+
widgets TEXT NOT NULL DEFAULT '[]',
|
|
28
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS reports (
|
|
32
|
+
id TEXT PRIMARY KEY,
|
|
33
|
+
name TEXT NOT NULL,
|
|
34
|
+
type TEXT NOT NULL CHECK(type IN ('daily','weekly','monthly','quarterly','annual','custom')),
|
|
35
|
+
content TEXT,
|
|
36
|
+
period TEXT,
|
|
37
|
+
generated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
38
|
+
metadata TEXT NOT NULL DEFAULT '{}'
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_kpis_name ON kpis(name);
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_kpis_category ON kpis(category);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_kpis_recorded_at ON kpis(recorded_at);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_kpis_name_recorded ON kpis(name, recorded_at);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_reports_type ON reports(type);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_reports_generated_at ON reports(generated_at);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_dashboards_name ON dashboards(name);
|
|
48
|
+
`,
|
|
49
|
+
},
|
|
50
|
+
];
|