@spfn/monitor 0.1.0-beta.22 → 0.1.0-beta.24

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/dist/index.js CHANGED
@@ -7,6 +7,7 @@ var monitorSchema = createSchema("@spfn/monitor");
7
7
 
8
8
  // src/server/entities/error-groups.ts
9
9
  import { text, integer, index } from "drizzle-orm/pg-core";
10
+ import { sql } from "drizzle-orm";
10
11
  import { id, timestamps, enumText, utcTimestamp } from "@spfn/core/db";
11
12
  var ERROR_GROUP_STATUSES = ["active", "resolved", "ignored"];
12
13
  var errorGroups = monitorSchema.table(
@@ -36,7 +37,15 @@ var errorGroups = monitorSchema.table(
36
37
  index("monitor_eg_fingerprint_idx").on(table.fingerprint),
37
38
  index("monitor_eg_status_idx").on(table.status),
38
39
  index("monitor_eg_last_seen_at_idx").on(table.lastSeenAt),
39
- index("monitor_eg_path_idx").on(table.path)
40
+ index("monitor_eg_path_idx").on(table.path),
41
+ // pg_trgm GIN indexes make the admin search's leading-wildcard ILIKE
42
+ // (%term%) on name/message/path sargable instead of a seq scan. Error
43
+ // groups are fingerprint-deduped, so insert volume is low — the write cost
44
+ // of these GIN indexes is acceptable here (NOT applied to the high-volume
45
+ // logs table). Requires the pg_trgm extension (see the migration).
46
+ index("monitor_eg_name_trgm_idx").using("gin", sql`${table.name} gin_trgm_ops`),
47
+ index("monitor_eg_message_trgm_idx").using("gin", sql`${table.message} gin_trgm_ops`),
48
+ index("monitor_eg_path_trgm_idx").using("gin", sql`${table.path} gin_trgm_ops`)
40
49
  ]
41
50
  );
42
51
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/server/entities/schema.ts","../src/server/entities/error-groups.ts","../src/server/entities/error-events.ts","../src/server/entities/logs.ts"],"sourcesContent":["/**\n * @spfn/monitor\n *\n * Error tracking, log management, and monitoring dashboard for SPFN\n *\n * @example\n * ```typescript\n * // Server-side\n * import { monitorRouter, createMonitorErrorHandler } from '@spfn/monitor/server';\n *\n * // Client-side (API calls)\n * import { monitorApi } from '@spfn/monitor';\n * const stats = await monitorApi.getStats.call({});\n * ```\n */\n\n// ============================================================================\n// API Client\n// ============================================================================\nimport { createApi } from '@spfn/core/nextjs';\nimport { monitorRouter } from './server/routes';\n\n/**\n * Type-safe API client for monitor routes\n *\n * @example\n * ```typescript\n * import { monitorApi } from '@spfn/monitor';\n *\n * // Get dashboard stats\n * const stats = await monitorApi.getStats.call({});\n *\n * // List errors\n * const errors = await monitorApi.listErrors.call({\n * query: { status: 'active', limit: 20 }\n * });\n * ```\n */\nexport const monitorApi = createApi<typeof monitorRouter>({});\n\n// Router type for external use\nexport type MonitorRouter = typeof monitorRouter;\n\n// ============================================================================\n// Shared Types (client-safe)\n// ============================================================================\nexport type {\n ErrorGroupStatus,\n LogLevel,\n} from './server/entities';\n\nexport type { MonitorStats } from './server/services/stats.service';\n\nexport {\n ERROR_GROUP_STATUSES,\n LOG_LEVELS,\n} from './server/entities';\n","/**\n * @spfn/monitor - Database Schema Definition\n *\n * Defines the 'spfn_monitor' PostgreSQL schema for all monitor-related tables\n */\n\nimport { createSchema } from '@spfn/core/db';\n\n/**\n * Monitor schema for all monitoring tables\n * Tables: error_groups, error_events, logs\n */\nexport const monitorSchema = createSchema('@spfn/monitor');\n","/**\n * @spfn/monitor - Error Groups Entity\n *\n * Groups errors by fingerprint (name + message + path) to avoid\n * duplicate tracking. Tracks count, status, and first/last seen times.\n */\n\nimport { text, integer, index } from 'drizzle-orm/pg-core';\nimport { id, timestamps, enumText, utcTimestamp } from '@spfn/core/db';\nimport { monitorSchema } from './schema';\n\n/**\n * Error group status types\n */\nexport const ERROR_GROUP_STATUSES = ['active', 'resolved', 'ignored'] as const;\nexport type ErrorGroupStatus = typeof ERROR_GROUP_STATUSES[number];\n\n/**\n * Error groups table — groups errors by fingerprint\n */\nexport const errorGroups = monitorSchema.table('error_groups',\n {\n // Primary Key\n id: id(),\n\n // Business Key — SHA-256 first 16 hex chars of (name:message:path)\n fingerprint: text('fingerprint').notNull().unique(),\n\n // Error identification\n name: text('name').notNull(),\n message: text('message').notNull(),\n path: text('path').notNull(),\n method: text('method').notNull(),\n statusCode: integer('status_code').notNull(),\n\n // Status\n status: enumText('status', ERROR_GROUP_STATUSES).default('active').notNull(),\n\n // Counters\n count: integer('count').notNull().default(1),\n\n // Timeline\n firstSeenAt: utcTimestamp('first_seen_at').notNull(),\n lastSeenAt: utcTimestamp('last_seen_at').notNull(),\n resolvedAt: utcTimestamp('resolved_at'),\n\n ...timestamps(),\n },\n (table) => [\n index('monitor_eg_fingerprint_idx').on(table.fingerprint),\n index('monitor_eg_status_idx').on(table.status),\n index('monitor_eg_last_seen_at_idx').on(table.lastSeenAt),\n index('monitor_eg_path_idx').on(table.path),\n ],\n);\n\nexport type ErrorGroup = typeof errorGroups.$inferSelect;\nexport type NewErrorGroup = typeof errorGroups.$inferInsert;\n","/**\n * @spfn/monitor - Error Events Entity\n *\n * Individual error occurrences linked to an error group.\n * Stores request-specific context (headers, query, stack trace).\n */\n\nimport { text, integer, jsonb, index } from 'drizzle-orm/pg-core';\nimport { id, timestamps, foreignKey } from '@spfn/core/db';\nimport { monitorSchema } from './schema';\nimport { errorGroups } from './error-groups';\n\n/**\n * Error events table — individual error occurrences\n */\nexport const errorEvents = monitorSchema.table('error_events',\n {\n // Primary Key\n id: id(),\n\n // Foreign Key\n groupId: foreignKey('group', () => errorGroups.id).notNull(),\n\n // Request context\n requestId: text('request_id'),\n userId: text('user_id'),\n statusCode: integer('status_code').notNull(),\n\n // Request details\n headers: jsonb('headers').$type<Record<string, string>>(),\n query: jsonb('query').$type<Record<string, string>>(),\n stackTrace: text('stack_trace'),\n metadata: jsonb('metadata').$type<Record<string, unknown>>(),\n\n ...timestamps(),\n },\n (table) => [\n index('monitor_ee_group_id_idx').on(table.groupId),\n index('monitor_ee_created_at_idx').on(table.createdAt),\n index('monitor_ee_user_id_idx').on(table.userId),\n ],\n);\n\nexport type ErrorEvent = typeof errorEvents.$inferSelect;\nexport type NewErrorEvent = typeof errorEvents.$inferInsert;\n","/**\n * @spfn/monitor - Logs Entity\n *\n * Developer logs stored in DB for retrieval via admin dashboard.\n * Supports level-based filtering, source tracking, and metadata.\n */\n\nimport { text, jsonb, index } from 'drizzle-orm/pg-core';\nimport { id, timestamps, enumText } from '@spfn/core/db';\nimport { monitorSchema } from './schema';\n\n/**\n * Log level types\n */\nexport const LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'fatal'] as const;\nexport type LogLevel = typeof LOG_LEVELS[number];\n\n/**\n * Logs table — developer log entries\n */\nexport const logs = monitorSchema.table('logs',\n {\n // Primary Key\n id: id(),\n\n // Log data\n level: enumText('level', LOG_LEVELS).notNull(),\n message: text('message').notNull(),\n source: text('source'),\n\n // Request context\n requestId: text('request_id'),\n userId: text('user_id'),\n\n // Extra data\n metadata: jsonb('metadata').$type<Record<string, unknown>>(),\n\n ...timestamps(),\n },\n (table) => [\n index('monitor_log_level_idx').on(table.level),\n index('monitor_log_source_idx').on(table.source),\n index('monitor_log_created_at_idx').on(table.createdAt),\n ],\n);\n\nexport type Log = typeof logs.$inferSelect;\nexport type NewLog = typeof logs.$inferInsert;\n"],"mappings":"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","names":["text","integer","index","id","timestamps","id","text","integer","timestamps","index","text","jsonb","index","id","timestamps","enumText","id","enumText","text","jsonb","timestamps","index"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/server/entities/schema.ts","../src/server/entities/error-groups.ts","../src/server/entities/error-events.ts","../src/server/entities/logs.ts"],"sourcesContent":["/**\n * @spfn/monitor\n *\n * Error tracking, log management, and monitoring dashboard for SPFN\n *\n * @example\n * ```typescript\n * // Server-side\n * import { monitorRouter, createMonitorErrorHandler } from '@spfn/monitor/server';\n *\n * // Client-side (API calls)\n * import { monitorApi } from '@spfn/monitor';\n * const stats = await monitorApi.getStats.call({});\n * ```\n */\n\n// ============================================================================\n// API Client\n// ============================================================================\nimport { createApi } from '@spfn/core/nextjs';\nimport { monitorRouter } from './server/routes';\n\n/**\n * Type-safe API client for monitor routes\n *\n * @example\n * ```typescript\n * import { monitorApi } from '@spfn/monitor';\n *\n * // Get dashboard stats\n * const stats = await monitorApi.getStats.call({});\n *\n * // List errors\n * const errors = await monitorApi.listErrors.call({\n * query: { status: 'active', limit: 20 }\n * });\n * ```\n */\nexport const monitorApi = createApi<typeof monitorRouter>({});\n\n// Router type for external use\nexport type MonitorRouter = typeof monitorRouter;\n\n// ============================================================================\n// Shared Types (client-safe)\n// ============================================================================\nexport type {\n ErrorGroupStatus,\n LogLevel,\n} from './server/entities';\n\nexport type { MonitorStats } from './server/services/stats.service';\n\nexport {\n ERROR_GROUP_STATUSES,\n LOG_LEVELS,\n} from './server/entities';\n","/**\n * @spfn/monitor - Database Schema Definition\n *\n * Defines the 'spfn_monitor' PostgreSQL schema for all monitor-related tables\n */\n\nimport { createSchema } from '@spfn/core/db';\n\n/**\n * Monitor schema for all monitoring tables\n * Tables: error_groups, error_events, logs\n */\nexport const monitorSchema = createSchema('@spfn/monitor');\n","/**\n * @spfn/monitor - Error Groups Entity\n *\n * Groups errors by fingerprint (name + message + path) to avoid\n * duplicate tracking. Tracks count, status, and first/last seen times.\n */\n\nimport { text, integer, index } from 'drizzle-orm/pg-core';\nimport { sql } from 'drizzle-orm';\nimport { id, timestamps, enumText, utcTimestamp } from '@spfn/core/db';\nimport { monitorSchema } from './schema';\n\n/**\n * Error group status types\n */\nexport const ERROR_GROUP_STATUSES = ['active', 'resolved', 'ignored'] as const;\nexport type ErrorGroupStatus = typeof ERROR_GROUP_STATUSES[number];\n\n/**\n * Error groups table — groups errors by fingerprint\n */\nexport const errorGroups = monitorSchema.table('error_groups',\n {\n // Primary Key\n id: id(),\n\n // Business Key — SHA-256 first 16 hex chars of (name:message:path)\n fingerprint: text('fingerprint').notNull().unique(),\n\n // Error identification\n name: text('name').notNull(),\n message: text('message').notNull(),\n path: text('path').notNull(),\n method: text('method').notNull(),\n statusCode: integer('status_code').notNull(),\n\n // Status\n status: enumText('status', ERROR_GROUP_STATUSES).default('active').notNull(),\n\n // Counters\n count: integer('count').notNull().default(1),\n\n // Timeline\n firstSeenAt: utcTimestamp('first_seen_at').notNull(),\n lastSeenAt: utcTimestamp('last_seen_at').notNull(),\n resolvedAt: utcTimestamp('resolved_at'),\n\n ...timestamps(),\n },\n (table) => [\n index('monitor_eg_fingerprint_idx').on(table.fingerprint),\n index('monitor_eg_status_idx').on(table.status),\n index('monitor_eg_last_seen_at_idx').on(table.lastSeenAt),\n index('monitor_eg_path_idx').on(table.path),\n // pg_trgm GIN indexes make the admin search's leading-wildcard ILIKE\n // (%term%) on name/message/path sargable instead of a seq scan. Error\n // groups are fingerprint-deduped, so insert volume is low — the write cost\n // of these GIN indexes is acceptable here (NOT applied to the high-volume\n // logs table). Requires the pg_trgm extension (see the migration).\n index('monitor_eg_name_trgm_idx').using('gin', sql`${table.name} gin_trgm_ops`),\n index('monitor_eg_message_trgm_idx').using('gin', sql`${table.message} gin_trgm_ops`),\n index('monitor_eg_path_trgm_idx').using('gin', sql`${table.path} gin_trgm_ops`),\n ],\n);\n\nexport type ErrorGroup = typeof errorGroups.$inferSelect;\nexport type NewErrorGroup = typeof errorGroups.$inferInsert;\n","/**\n * @spfn/monitor - Error Events Entity\n *\n * Individual error occurrences linked to an error group.\n * Stores request-specific context (headers, query, stack trace).\n */\n\nimport { text, integer, jsonb, index } from 'drizzle-orm/pg-core';\nimport { id, timestamps, foreignKey } from '@spfn/core/db';\nimport { monitorSchema } from './schema';\nimport { errorGroups } from './error-groups';\n\n/**\n * Error events table — individual error occurrences\n */\nexport const errorEvents = monitorSchema.table('error_events',\n {\n // Primary Key\n id: id(),\n\n // Foreign Key\n groupId: foreignKey('group', () => errorGroups.id).notNull(),\n\n // Request context\n requestId: text('request_id'),\n userId: text('user_id'),\n statusCode: integer('status_code').notNull(),\n\n // Request details\n headers: jsonb('headers').$type<Record<string, string>>(),\n query: jsonb('query').$type<Record<string, string>>(),\n stackTrace: text('stack_trace'),\n metadata: jsonb('metadata').$type<Record<string, unknown>>(),\n\n ...timestamps(),\n },\n (table) => [\n index('monitor_ee_group_id_idx').on(table.groupId),\n index('monitor_ee_created_at_idx').on(table.createdAt),\n index('monitor_ee_user_id_idx').on(table.userId),\n ],\n);\n\nexport type ErrorEvent = typeof errorEvents.$inferSelect;\nexport type NewErrorEvent = typeof errorEvents.$inferInsert;\n","/**\n * @spfn/monitor - Logs Entity\n *\n * Developer logs stored in DB for retrieval via admin dashboard.\n * Supports level-based filtering, source tracking, and metadata.\n */\n\nimport { text, jsonb, index } from 'drizzle-orm/pg-core';\nimport { id, timestamps, enumText } from '@spfn/core/db';\nimport { monitorSchema } from './schema';\n\n/**\n * Log level types\n */\nexport const LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'fatal'] as const;\nexport type LogLevel = typeof LOG_LEVELS[number];\n\n/**\n * Logs table — developer log entries\n */\nexport const logs = monitorSchema.table('logs',\n {\n // Primary Key\n id: id(),\n\n // Log data\n level: enumText('level', LOG_LEVELS).notNull(),\n message: text('message').notNull(),\n source: text('source'),\n\n // Request context\n requestId: text('request_id'),\n userId: text('user_id'),\n\n // Extra data\n metadata: jsonb('metadata').$type<Record<string, unknown>>(),\n\n ...timestamps(),\n },\n (table) => [\n index('monitor_log_level_idx').on(table.level),\n index('monitor_log_source_idx').on(table.source),\n index('monitor_log_created_at_idx').on(table.createdAt),\n ],\n);\n\nexport type Log = typeof logs.$inferSelect;\nexport type NewLog = typeof logs.$inferInsert;\n"],"mappings":"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","names":["text","integer","index","id","timestamps","id","text","integer","timestamps","index","text","jsonb","index","id","timestamps","enumText","id","enumText","text","jsonb","timestamps","index"]}
package/dist/server.d.ts CHANGED
@@ -291,6 +291,13 @@ declare class ErrorGroupsRepository extends BaseRepository {
291
291
  findMany(filters?: ErrorGroupFilters): Promise<ErrorGroup[]>;
292
292
  create(data: NewErrorGroup): Promise<ErrorGroup>;
293
293
  incrementCount(id: number): Promise<ErrorGroup | null>;
294
+ /**
295
+ * Reopen a resolved group: set active, clear resolvedAt, and bump count +
296
+ * lastSeenAt in a SINGLE UPDATE (the reopen path used to do updateStatus then
297
+ * incrementCount — two round trips writing the same row, the second clobbering
298
+ * the first).
299
+ */
300
+ reactivate(id: number): Promise<ErrorGroup | null>;
294
301
  updateStatus(id: number, status: ErrorGroupStatus): Promise<ErrorGroup | null>;
295
302
  countByStatus(): Promise<Record<ErrorGroupStatus, number>>;
296
303
  }
package/dist/server.js CHANGED
@@ -2628,7 +2628,7 @@ var Type = type_exports2;
2628
2628
  import { route } from "@spfn/core/route";
2629
2629
 
2630
2630
  // src/server/repositories/error-groups.repository.ts
2631
- import { eq, and, desc, sql, ilike, or, gte, lte } from "drizzle-orm";
2631
+ import { eq, and, desc, sql as sql2, ilike, or, gte, lte } from "drizzle-orm";
2632
2632
  import { BaseRepository } from "@spfn/core/db";
2633
2633
 
2634
2634
  // src/server/entities/schema.ts
@@ -2637,6 +2637,7 @@ var monitorSchema = createSchema("@spfn/monitor");
2637
2637
 
2638
2638
  // src/server/entities/error-groups.ts
2639
2639
  import { text, integer, index } from "drizzle-orm/pg-core";
2640
+ import { sql } from "drizzle-orm";
2640
2641
  import { id, timestamps, enumText, utcTimestamp } from "@spfn/core/db";
2641
2642
  var ERROR_GROUP_STATUSES = ["active", "resolved", "ignored"];
2642
2643
  var errorGroups = monitorSchema.table(
@@ -2666,7 +2667,15 @@ var errorGroups = monitorSchema.table(
2666
2667
  index("monitor_eg_fingerprint_idx").on(table.fingerprint),
2667
2668
  index("monitor_eg_status_idx").on(table.status),
2668
2669
  index("monitor_eg_last_seen_at_idx").on(table.lastSeenAt),
2669
- index("monitor_eg_path_idx").on(table.path)
2670
+ index("monitor_eg_path_idx").on(table.path),
2671
+ // pg_trgm GIN indexes make the admin search's leading-wildcard ILIKE
2672
+ // (%term%) on name/message/path sargable instead of a seq scan. Error
2673
+ // groups are fingerprint-deduped, so insert volume is low — the write cost
2674
+ // of these GIN indexes is acceptable here (NOT applied to the high-volume
2675
+ // logs table). Requires the pg_trgm extension (see the migration).
2676
+ index("monitor_eg_name_trgm_idx").using("gin", sql`${table.name} gin_trgm_ops`),
2677
+ index("monitor_eg_message_trgm_idx").using("gin", sql`${table.message} gin_trgm_ops`),
2678
+ index("monitor_eg_path_trgm_idx").using("gin", sql`${table.path} gin_trgm_ops`)
2670
2679
  ]
2671
2680
  );
2672
2681
 
@@ -2780,12 +2789,29 @@ var ErrorGroupsRepository = class extends BaseRepository {
2780
2789
  }
2781
2790
  async incrementCount(id4) {
2782
2791
  const result = await this.db.update(errorGroups).set({
2783
- count: sql`${errorGroups.count} + 1`,
2792
+ count: sql2`${errorGroups.count} + 1`,
2784
2793
  lastSeenAt: /* @__PURE__ */ new Date(),
2785
2794
  updatedAt: /* @__PURE__ */ new Date()
2786
2795
  }).where(eq(errorGroups.id, id4)).returning();
2787
2796
  return result[0] ?? null;
2788
2797
  }
2798
+ /**
2799
+ * Reopen a resolved group: set active, clear resolvedAt, and bump count +
2800
+ * lastSeenAt in a SINGLE UPDATE (the reopen path used to do updateStatus then
2801
+ * incrementCount — two round trips writing the same row, the second clobbering
2802
+ * the first).
2803
+ */
2804
+ async reactivate(id4) {
2805
+ const now = /* @__PURE__ */ new Date();
2806
+ const result = await this.db.update(errorGroups).set({
2807
+ status: "active",
2808
+ resolvedAt: null,
2809
+ count: sql2`${errorGroups.count} + 1`,
2810
+ lastSeenAt: now,
2811
+ updatedAt: now
2812
+ }).where(eq(errorGroups.id, id4)).returning();
2813
+ return result[0] ?? null;
2814
+ }
2789
2815
  async updateStatus(id4, status) {
2790
2816
  const now = /* @__PURE__ */ new Date();
2791
2817
  const result = await this.db.update(errorGroups).set({
@@ -2798,7 +2824,7 @@ var ErrorGroupsRepository = class extends BaseRepository {
2798
2824
  async countByStatus() {
2799
2825
  const result = await this.readDb.select({
2800
2826
  status: errorGroups.status,
2801
- count: sql`count(*)::int`
2827
+ count: sql2`count(*)::int`
2802
2828
  }).from(errorGroups).groupBy(errorGroups.status);
2803
2829
  const counts = {
2804
2830
  active: 0,
@@ -2829,14 +2855,14 @@ var ErrorEventsRepository = class extends BaseRepository2 {
2829
2855
  return await this.readDb.select().from(errorEvents).where(eq2(errorEvents.groupId, groupId)).orderBy(desc2(errorEvents.createdAt)).limit(limit).offset(offset);
2830
2856
  }
2831
2857
  async deleteOlderThan(date) {
2832
- const result = await this.db.delete(errorEvents).where(lt(errorEvents.createdAt, date)).returning();
2833
- return result.length;
2858
+ const result = await this.db.delete(errorEvents).where(lt(errorEvents.createdAt, date));
2859
+ return result.rowCount ?? result.count ?? 0;
2834
2860
  }
2835
2861
  };
2836
2862
  var errorEventsRepository = new ErrorEventsRepository();
2837
2863
 
2838
2864
  // src/server/repositories/logs.repository.ts
2839
- import { eq as eq3, and as and2, desc as desc3, lt as lt2, ilike as ilike2, or as or2, gte as gte2, lte as lte2, sql as sql2 } from "drizzle-orm";
2865
+ import { eq as eq3, and as and2, desc as desc3, lt as lt2, ilike as ilike2, or as or2, gte as gte2, lte as lte2, sql as sql3 } from "drizzle-orm";
2840
2866
  import { BaseRepository as BaseRepository3 } from "@spfn/core/db";
2841
2867
  var LogsRepository = class extends BaseRepository3 {
2842
2868
  async create(data) {
@@ -2890,7 +2916,7 @@ var LogsRepository = class extends BaseRepository3 {
2890
2916
  async countByLevel() {
2891
2917
  const result = await this.readDb.select({
2892
2918
  level: logs.level,
2893
- count: sql2`count(*)::int`
2919
+ count: sql3`count(*)::int`
2894
2920
  }).from(logs).groupBy(logs.level);
2895
2921
  const counts = {
2896
2922
  debug: 0,
@@ -2905,8 +2931,8 @@ var LogsRepository = class extends BaseRepository3 {
2905
2931
  return counts;
2906
2932
  }
2907
2933
  async deleteOlderThan(date) {
2908
- const result = await this.db.delete(logs).where(lt2(logs.createdAt, date)).returning();
2909
- return result.length;
2934
+ const result = await this.db.delete(logs).where(lt2(logs.createdAt, date));
2935
+ return result.rowCount ?? result.count ?? 0;
2910
2936
  }
2911
2937
  };
2912
2938
  var logsRepository = new LogsRepository();
@@ -3053,8 +3079,7 @@ async function trackError(err, ctx, metadata) {
3053
3079
  return;
3054
3080
  }
3055
3081
  if (existing.status === "resolved") {
3056
- await errorGroupsRepository.updateStatus(existing.id, "active");
3057
- await errorGroupsRepository.incrementCount(existing.id);
3082
+ await errorGroupsRepository.reactivate(existing.id);
3058
3083
  const event = await safeCreateEvent(existing.id, err, ctx, metadata);
3059
3084
  logger2.info("Error group reopened", { fingerprint, groupId: existing.id });
3060
3085
  if (event) {
@@ -3141,7 +3166,7 @@ async function queryLogs(filters) {
3141
3166
  }
3142
3167
 
3143
3168
  // src/server/services/stats.service.ts
3144
- import { sql as sql3, gte as gte3 } from "drizzle-orm";
3169
+ import { sql as sql4, gte as gte3 } from "drizzle-orm";
3145
3170
  import { getDatabase } from "@spfn/core/db";
3146
3171
  async function getMonitorStats() {
3147
3172
  const [statusCounts, recentErrors, levelCounts, trends] = await Promise.all([
@@ -3171,9 +3196,9 @@ async function getTrends() {
3171
3196
  const last24h = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
3172
3197
  const last7d = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
3173
3198
  const [errorsLast24h, errorsLast7d, logsLast24h] = await Promise.all([
3174
- db.select({ count: sql3`count(*)::int` }).from(errorEvents).where(gte3(errorEvents.createdAt, last24h)).then((r) => r[0]?.count ?? 0),
3175
- db.select({ count: sql3`count(*)::int` }).from(errorEvents).where(gte3(errorEvents.createdAt, last7d)).then((r) => r[0]?.count ?? 0),
3176
- db.select({ count: sql3`count(*)::int` }).from(logs).where(gte3(logs.createdAt, last24h)).then((r) => r[0]?.count ?? 0)
3199
+ db.select({ count: sql4`count(*)::int` }).from(errorEvents).where(gte3(errorEvents.createdAt, last24h)).then((r) => r[0]?.count ?? 0),
3200
+ db.select({ count: sql4`count(*)::int` }).from(errorEvents).where(gte3(errorEvents.createdAt, last7d)).then((r) => r[0]?.count ?? 0),
3201
+ db.select({ count: sql4`count(*)::int` }).from(logs).where(gte3(logs.createdAt, last24h)).then((r) => r[0]?.count ?? 0)
3177
3202
  ]);
3178
3203
  return { errorsLast24h, errorsLast7d, logsLast24h };
3179
3204
  }