@hasna/logs 0.3.25 → 0.3.27

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.
Files changed (132) hide show
  1. package/README.md +33 -10
  2. package/dashboard/dist/assets/index-C0wZYq1m.js +53 -0
  3. package/dashboard/dist/assets/index-DGNrK5qb.css +1 -0
  4. package/dashboard/dist/index.html +14 -0
  5. package/dist/cli/index.js +8511 -177
  6. package/dist/count-bmj4r2zb.js +10 -0
  7. package/dist/{diagnose-e0w5rwbc.js → diagnose-3q5cy9ra.js} +2 -2
  8. package/dist/{export-c3eqjste.js → export-cngdb9fh.js} +1 -1
  9. package/dist/{http-zm3ph78w.js → http-r0xc3d2s.js} +79 -8
  10. package/dist/index-931pbyn5.js +141 -0
  11. package/dist/index-b5c72f1p.js +7 -0
  12. package/dist/{index-p1vgwwsz.js → index-bnr19y0h.js} +596 -37
  13. package/dist/{index-7w7v7hnr.js → index-by1pdzbr.js} +14 -5
  14. package/dist/{index-3dr7d80h.js → index-e1930v9b.js} +12 -8
  15. package/dist/{index-eh9bkbpa.js → index-e72k53yq.js} +10 -2
  16. package/dist/{index-edn08m6f.js → index-gcd14q2f.js} +9 -6
  17. package/dist/index-hq6kzaah.js +26 -0
  18. package/dist/index-j34f36wy.js +5672 -0
  19. package/dist/index-p4dbdzx4.js +1849 -0
  20. package/dist/{index-5qznfyah.js → index-q27bgpr1.js} +1086 -1646
  21. package/dist/index-t3x838zw.js +2583 -0
  22. package/dist/{index-ww5ggfv3.js → index-zkb3z95a.js} +12 -9
  23. package/dist/index.js +2982 -22
  24. package/dist/{jobs-ypmmc2ma.js → jobs-hsgyhfvm.js} +2 -1
  25. package/dist/mcp/index.js +1473 -4286
  26. package/dist/{query-7jwj05er.js → query-c5a43zx3.js} +3 -2
  27. package/dist/server/index.js +2944 -417
  28. package/dist/storage.js +50 -0
  29. package/package.json +27 -8
  30. package/biome.json +0 -13
  31. package/bun.lock +0 -376
  32. package/dashboard/README.md +0 -73
  33. package/dashboard/bun.lock +0 -526
  34. package/dashboard/eslint.config.js +0 -23
  35. package/dashboard/index.html +0 -13
  36. package/dashboard/package.json +0 -32
  37. package/dashboard/src/App.css +0 -184
  38. package/dashboard/src/App.tsx +0 -49
  39. package/dashboard/src/api.ts +0 -33
  40. package/dashboard/src/assets/hero.png +0 -0
  41. package/dashboard/src/assets/react.svg +0 -1
  42. package/dashboard/src/assets/vite.svg +0 -1
  43. package/dashboard/src/index.css +0 -111
  44. package/dashboard/src/main.tsx +0 -10
  45. package/dashboard/src/pages/Alerts.tsx +0 -69
  46. package/dashboard/src/pages/Issues.tsx +0 -50
  47. package/dashboard/src/pages/Perf.tsx +0 -75
  48. package/dashboard/src/pages/Projects.tsx +0 -67
  49. package/dashboard/src/pages/Summary.tsx +0 -67
  50. package/dashboard/src/pages/Tail.tsx +0 -65
  51. package/dashboard/tsconfig.app.json +0 -28
  52. package/dashboard/tsconfig.json +0 -7
  53. package/dashboard/tsconfig.node.json +0 -26
  54. package/dashboard/vite.config.ts +0 -14
  55. package/dist/count-x3n7qg3c.js +0 -9
  56. package/dist/index-5cj74qka.js +0 -10803
  57. package/dist/index-997bkzr2.js +0 -15
  58. package/dist/index-kezb178p.js +0 -1241
  59. package/dist/index-pen6t0yc.js +0 -10794
  60. package/sdk/package.json +0 -27
  61. package/sdk/src/index.ts +0 -143
  62. package/sdk/src/types.ts +0 -56
  63. package/src/cli/entrypoints.test.ts +0 -63
  64. package/src/cli/index.ts +0 -471
  65. package/src/db/index.test.ts +0 -33
  66. package/src/db/index.ts +0 -189
  67. package/src/db/migrations/001_alert_rules.ts +0 -21
  68. package/src/db/migrations/002_issues.ts +0 -21
  69. package/src/db/migrations/003_retention.ts +0 -15
  70. package/src/db/migrations/004_page_auth.ts +0 -13
  71. package/src/db/pg-migrations.ts +0 -167
  72. package/src/index.ts +0 -1
  73. package/src/lib/alerts.test.ts +0 -67
  74. package/src/lib/alerts.ts +0 -117
  75. package/src/lib/browser-script.test.ts +0 -35
  76. package/src/lib/browser-script.ts +0 -31
  77. package/src/lib/compare.test.ts +0 -52
  78. package/src/lib/compare.ts +0 -85
  79. package/src/lib/count.test.ts +0 -44
  80. package/src/lib/count.ts +0 -55
  81. package/src/lib/diagnose.test.ts +0 -55
  82. package/src/lib/diagnose.ts +0 -91
  83. package/src/lib/export.test.ts +0 -66
  84. package/src/lib/export.ts +0 -65
  85. package/src/lib/github.ts +0 -38
  86. package/src/lib/health.test.ts +0 -48
  87. package/src/lib/health.ts +0 -51
  88. package/src/lib/ingest.test.ts +0 -57
  89. package/src/lib/ingest.ts +0 -78
  90. package/src/lib/issues.test.ts +0 -79
  91. package/src/lib/issues.ts +0 -70
  92. package/src/lib/jobs.test.ts +0 -69
  93. package/src/lib/jobs.ts +0 -63
  94. package/src/lib/lighthouse.ts +0 -65
  95. package/src/lib/package-meta.test.ts +0 -43
  96. package/src/lib/package-meta.ts +0 -80
  97. package/src/lib/page-auth.test.ts +0 -54
  98. package/src/lib/page-auth.ts +0 -48
  99. package/src/lib/parse-time.test.ts +0 -37
  100. package/src/lib/parse-time.ts +0 -14
  101. package/src/lib/perf.test.ts +0 -45
  102. package/src/lib/perf.ts +0 -46
  103. package/src/lib/projects.test.ts +0 -73
  104. package/src/lib/projects.ts +0 -69
  105. package/src/lib/query.test.ts +0 -104
  106. package/src/lib/query.ts +0 -84
  107. package/src/lib/retention.test.ts +0 -42
  108. package/src/lib/retention.ts +0 -62
  109. package/src/lib/rotate.test.ts +0 -37
  110. package/src/lib/rotate.ts +0 -27
  111. package/src/lib/scanner.ts +0 -131
  112. package/src/lib/scheduler.ts +0 -63
  113. package/src/lib/session-context.ts +0 -28
  114. package/src/lib/summarize.test.ts +0 -38
  115. package/src/lib/summarize.ts +0 -23
  116. package/src/mcp/http.test.ts +0 -92
  117. package/src/mcp/http.ts +0 -135
  118. package/src/mcp/index.test.ts +0 -27
  119. package/src/mcp/index.ts +0 -444
  120. package/src/server/index.ts +0 -61
  121. package/src/server/routes/alerts.ts +0 -32
  122. package/src/server/routes/issues.ts +0 -43
  123. package/src/server/routes/jobs.ts +0 -32
  124. package/src/server/routes/logs.ts +0 -113
  125. package/src/server/routes/perf.ts +0 -23
  126. package/src/server/routes/projects.ts +0 -67
  127. package/src/server/routes/stream.ts +0 -43
  128. package/src/server/server.test.ts +0 -194
  129. package/src/types/index.ts +0 -119
  130. package/tsconfig.json +0 -22
  131. /package/dashboard/{public → dist}/favicon.svg +0 -0
  132. /package/dashboard/{public → dist}/icons.svg +0 -0
@@ -1,57 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { createTestDb } from "../db/index.ts"
3
- import { ingestBatch, ingestLog } from "./ingest.ts"
4
-
5
- describe("ingest", () => {
6
- it("inserts a single log entry", () => {
7
- const db = createTestDb()
8
- const row = ingestLog(db, { level: "error", message: "test error", service: "api" })
9
- expect(row.id).toBeTruthy()
10
- expect(row.level).toBe("error")
11
- expect(row.message).toBe("test error")
12
- expect(row.service).toBe("api")
13
- expect(row.source).toBe("sdk")
14
- expect(row.timestamp).toBeTruthy()
15
- })
16
-
17
- it("inserts with all optional fields", () => {
18
- const db = createTestDb()
19
- const row = ingestLog(db, {
20
- level: "info",
21
- message: "hello",
22
- source: "scanner",
23
- trace_id: "trace-123",
24
- session_id: "sess-456",
25
- agent: "brutus",
26
- url: "https://example.com",
27
- stack_trace: "Error at line 1",
28
- metadata: { foo: "bar" },
29
- })
30
- expect(row.trace_id).toBe("trace-123")
31
- expect(row.agent).toBe("brutus")
32
- expect(row.metadata).toBe(JSON.stringify({ foo: "bar" }))
33
- })
34
-
35
- it("inserts a batch", () => {
36
- const db = createTestDb()
37
- const rows = ingestBatch(db, [
38
- { level: "warn", message: "warn 1" },
39
- { level: "error", message: "err 1" },
40
- { level: "info", message: "info 1" },
41
- ])
42
- expect(rows).toHaveLength(3)
43
- expect(rows[0]!.level).toBe("warn")
44
- expect(rows[2]!.level).toBe("info")
45
- })
46
-
47
- it("batch is transactional", () => {
48
- const db = createTestDb()
49
- const before = (db.prepare("SELECT COUNT(*) as c FROM logs").get() as { c: number }).c
50
- ingestBatch(db, [
51
- { level: "debug", message: "a" },
52
- { level: "fatal", message: "b" },
53
- ])
54
- const after = (db.prepare("SELECT COUNT(*) as c FROM logs").get() as { c: number }).c
55
- expect(after - before).toBe(2)
56
- })
57
- })
package/src/lib/ingest.ts DELETED
@@ -1,78 +0,0 @@
1
- import type { DbAdapter } from "@hasna/cloud"
2
- import type { LogEntry, LogRow } from "../types/index.ts"
3
- import { upsertIssue } from "./issues.ts"
4
- import { evaluateAlerts } from "./alerts.ts"
5
-
6
- const ERROR_LEVELS = new Set(["warn", "error", "fatal"])
7
-
8
- export function ingestLog(db: DbAdapter, entry: LogEntry): LogRow {
9
- const stmt = db.prepare(`
10
- INSERT INTO logs (project_id, page_id, level, source, service, message, trace_id, session_id, agent, url, stack_trace, metadata)
11
- VALUES ($project_id, $page_id, $level, $source, $service, $message, $trace_id, $session_id, $agent, $url, $stack_trace, $metadata)
12
- RETURNING *
13
- `)
14
- const row = stmt.get({
15
- $project_id: entry.project_id ?? null,
16
- $page_id: entry.page_id ?? null,
17
- $level: entry.level,
18
- $source: entry.source ?? "sdk",
19
- $service: entry.service ?? null,
20
- $message: entry.message,
21
- $trace_id: entry.trace_id ?? null,
22
- $session_id: entry.session_id ?? null,
23
- $agent: entry.agent ?? null,
24
- $url: entry.url ?? null,
25
- $stack_trace: entry.stack_trace ?? null,
26
- $metadata: entry.metadata ? JSON.stringify(entry.metadata) : null,
27
- }) as LogRow
28
-
29
- // Side effects: issue grouping + alert evaluation (fire-and-forget)
30
- if (ERROR_LEVELS.has(entry.level)) {
31
- if (entry.project_id) {
32
- upsertIssue(db, { project_id: entry.project_id, level: entry.level, service: entry.service, message: entry.message, stack_trace: entry.stack_trace })
33
- evaluateAlerts(db, entry.project_id, entry.service ?? null, entry.level).catch(() => {})
34
- }
35
- }
36
-
37
- return row
38
- }
39
-
40
- export function ingestBatch(db: DbAdapter, entries: LogEntry[], sharedTraceId?: string | null): LogRow[] {
41
- // Apply shared trace_id to entries that don't have their own
42
- if (sharedTraceId) {
43
- entries = entries.map(e => e.trace_id ? e : { ...e, trace_id: sharedTraceId })
44
- }
45
- const insert = db.prepare(`
46
- INSERT INTO logs (project_id, page_id, level, source, service, message, trace_id, session_id, agent, url, stack_trace, metadata)
47
- VALUES ($project_id, $page_id, $level, $source, $service, $message, $trace_id, $session_id, $agent, $url, $stack_trace, $metadata)
48
- RETURNING *
49
- `)
50
- // @hasna/cloud executes the callback inside the transaction immediately.
51
- const rows = db.transaction(() =>
52
- entries.map(entry =>
53
- insert.get({
54
- $project_id: entry.project_id ?? null,
55
- $page_id: entry.page_id ?? null,
56
- $level: entry.level,
57
- $source: entry.source ?? "sdk",
58
- $service: entry.service ?? null,
59
- $message: entry.message,
60
- $trace_id: entry.trace_id ?? null,
61
- $session_id: entry.session_id ?? null,
62
- $agent: entry.agent ?? null,
63
- $url: entry.url ?? null,
64
- $stack_trace: entry.stack_trace ?? null,
65
- $metadata: entry.metadata ? JSON.stringify(entry.metadata) : null,
66
- }) as LogRow
67
- )
68
- )
69
-
70
- // Issue grouping for error-level entries (outside transaction for perf)
71
- for (const entry of entries) {
72
- if (ERROR_LEVELS.has(entry.level) && entry.project_id) {
73
- upsertIssue(db, { project_id: entry.project_id, level: entry.level, service: entry.service, message: entry.message, stack_trace: entry.stack_trace })
74
- }
75
- }
76
-
77
- return rows
78
- }
@@ -1,79 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { createTestDb } from "../db/index.ts"
3
- import { computeFingerprint, getIssue, listIssues, updateIssueStatus, upsertIssue } from "./issues.ts"
4
-
5
- function seedProject(db: ReturnType<typeof createTestDb>) {
6
- return db.prepare("INSERT INTO projects (name) VALUES ('app') RETURNING id").get() as { id: string }
7
- }
8
-
9
- describe("computeFingerprint", () => {
10
- it("returns consistent hash for same input", () => {
11
- const a = computeFingerprint("error", "api", "DB connection failed")
12
- const b = computeFingerprint("error", "api", "DB connection failed")
13
- expect(a).toBe(b)
14
- })
15
-
16
- it("returns different hash for different messages", () => {
17
- const a = computeFingerprint("error", "api", "timeout")
18
- const b = computeFingerprint("error", "api", "DB error")
19
- expect(a).not.toBe(b)
20
- })
21
-
22
- it("normalizes hex IDs in messages", () => {
23
- const a = computeFingerprint("error", "api", "Error for id abc123def456")
24
- const b = computeFingerprint("error", "api", "Error for id 000fffaabbcc")
25
- expect(a).toBe(b)
26
- })
27
- })
28
-
29
- describe("upsertIssue", () => {
30
- it("creates a new issue", () => {
31
- const db = createTestDb()
32
- const p = seedProject(db)
33
- const issue = upsertIssue(db, { project_id: p.id, level: "error", service: "api", message: "DB timeout" })
34
- expect(issue.id).toBeTruthy()
35
- expect(issue.count).toBe(1)
36
- expect(issue.status).toBe("open")
37
- })
38
-
39
- it("increments count on duplicate", () => {
40
- const db = createTestDb()
41
- const p = seedProject(db)
42
- upsertIssue(db, { project_id: p.id, level: "error", message: "Same error" })
43
- upsertIssue(db, { project_id: p.id, level: "error", message: "Same error" })
44
- const issue = upsertIssue(db, { project_id: p.id, level: "error", message: "Same error" })
45
- expect(issue.count).toBe(3)
46
- })
47
-
48
- it("reopens resolved issues", () => {
49
- const db = createTestDb()
50
- const p = seedProject(db)
51
- const issue = upsertIssue(db, { project_id: p.id, level: "error", message: "err" })
52
- updateIssueStatus(db, issue.id, "resolved")
53
- const reopened = upsertIssue(db, { project_id: p.id, level: "error", message: "err" })
54
- expect(reopened.status).toBe("open")
55
- })
56
- })
57
-
58
- describe("listIssues", () => {
59
- it("filters by project and status", () => {
60
- const db = createTestDb()
61
- const p = seedProject(db)
62
- const issue = upsertIssue(db, { project_id: p.id, level: "error", message: "database connection timed out" })
63
- updateIssueStatus(db, issue.id, "resolved")
64
- upsertIssue(db, { project_id: p.id, level: "error", message: "authentication service unavailable" })
65
- expect(listIssues(db, p.id, "open")).toHaveLength(1)
66
- expect(listIssues(db, p.id, "resolved")).toHaveLength(1)
67
- expect(listIssues(db, p.id)).toHaveLength(2)
68
- })
69
- })
70
-
71
- describe("updateIssueStatus", () => {
72
- it("updates status", () => {
73
- const db = createTestDb()
74
- const p = seedProject(db)
75
- const issue = upsertIssue(db, { project_id: p.id, level: "error", message: "x" })
76
- const updated = updateIssueStatus(db, issue.id, "ignored")
77
- expect(updated?.status).toBe("ignored")
78
- })
79
- })
package/src/lib/issues.ts DELETED
@@ -1,70 +0,0 @@
1
- import type { Database } from "bun:sqlite"
2
- import { createHash } from "node:crypto"
3
-
4
- export interface Issue {
5
- id: string
6
- project_id: string | null
7
- fingerprint: string
8
- level: string
9
- service: string | null
10
- message_template: string
11
- first_seen: string
12
- last_seen: string
13
- count: number
14
- status: "open" | "resolved" | "ignored"
15
- }
16
-
17
- export function computeFingerprint(level: string, service: string | null, message: string, stackTrace?: string | null): string {
18
- // Normalize message: strip hex IDs, numbers, timestamps
19
- const normalized = message
20
- .replace(/[0-9a-f]{8,}/gi, "<id>")
21
- .replace(/\d+/g, "<n>")
22
- .replace(/https?:\/\/[^\s]+/g, "<url>")
23
- .trim()
24
- const stackFrame = stackTrace ? stackTrace.split("\n").slice(0, 3).join("|") : ""
25
- const raw = `${level}|${service ?? ""}|${normalized}|${stackFrame}`
26
- return createHash("sha256").update(raw).digest("hex").slice(0, 16)
27
- }
28
-
29
- export function upsertIssue(db: Database, data: {
30
- project_id?: string
31
- level: string
32
- service?: string | null
33
- message: string
34
- stack_trace?: string | null
35
- }): Issue {
36
- const fingerprint = computeFingerprint(data.level, data.service ?? null, data.message, data.stack_trace)
37
- return db.prepare(`
38
- INSERT INTO issues (project_id, fingerprint, level, service, message_template)
39
- VALUES ($project_id, $fingerprint, $level, $service, $message_template)
40
- ON CONFLICT(project_id, fingerprint) DO UPDATE SET
41
- count = count + 1,
42
- last_seen = strftime('%Y-%m-%dT%H:%M:%fZ','now'),
43
- status = CASE WHEN status = 'resolved' THEN 'open' ELSE status END
44
- RETURNING *
45
- `).get({
46
- $project_id: data.project_id ?? null,
47
- $fingerprint: fingerprint,
48
- $level: data.level,
49
- $service: data.service ?? null,
50
- $message_template: data.message.slice(0, 500),
51
- }) as Issue
52
- }
53
-
54
- export function listIssues(db: Database, projectId?: string, status?: string, limit = 50): Issue[] {
55
- const conditions: string[] = []
56
- const params: Record<string, unknown> = { $limit: limit }
57
- if (projectId) { conditions.push("project_id = $p"); params.$p = projectId }
58
- if (status) { conditions.push("status = $status"); params.$status = status }
59
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""
60
- return db.prepare(`SELECT * FROM issues ${where} ORDER BY last_seen DESC LIMIT $limit`).all(params) as Issue[]
61
- }
62
-
63
- export function getIssue(db: Database, id: string): Issue | null {
64
- return db.prepare("SELECT * FROM issues WHERE id = $id").get({ $id: id }) as Issue | null
65
- }
66
-
67
- export function updateIssueStatus(db: Database, id: string, status: "open" | "resolved" | "ignored"): Issue | null {
68
- return db.prepare("UPDATE issues SET status = $status WHERE id = $id RETURNING *")
69
- .get({ $id: id, $status: status }) as Issue | null
70
- }
@@ -1,69 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { createTestDb } from "../db/index.ts"
3
- import { createJob, createScanRun, deleteJob, finishScanRun, listJobs, listScanRuns, updateJob } from "./jobs.ts"
4
-
5
- function seedProject(db: ReturnType<typeof createTestDb>) {
6
- return db.prepare("INSERT INTO projects (name) VALUES ('test') RETURNING id").get() as { id: string }
7
- }
8
-
9
- describe("jobs", () => {
10
- it("creates a job", () => {
11
- const db = createTestDb()
12
- const p = seedProject(db)
13
- const job = createJob(db, { project_id: p.id, schedule: "*/5 * * * *" })
14
- expect(job.id).toBeTruthy()
15
- expect(job.schedule).toBe("*/5 * * * *")
16
- expect(job.enabled).toBe(1)
17
- })
18
-
19
- it("lists jobs for a project", () => {
20
- const db = createTestDb()
21
- const p = seedProject(db)
22
- createJob(db, { project_id: p.id, schedule: "*/5 * * * *" })
23
- createJob(db, { project_id: p.id, schedule: "*/10 * * * *" })
24
- expect(listJobs(db, p.id)).toHaveLength(2)
25
- })
26
-
27
- it("updates a job", () => {
28
- const db = createTestDb()
29
- const p = seedProject(db)
30
- const job = createJob(db, { project_id: p.id, schedule: "*/5 * * * *" })
31
- const updated = updateJob(db, job.id, { enabled: 0 })
32
- expect(updated?.enabled).toBe(0)
33
- })
34
-
35
- it("deletes a job", () => {
36
- const db = createTestDb()
37
- const p = seedProject(db)
38
- const job = createJob(db, { project_id: p.id, schedule: "*/5 * * * *" })
39
- deleteJob(db, job.id)
40
- expect(listJobs(db, p.id)).toHaveLength(0)
41
- })
42
- })
43
-
44
- describe("scan runs", () => {
45
- it("creates and finishes a scan run", () => {
46
- const db = createTestDb()
47
- const p = seedProject(db)
48
- const job = createJob(db, { project_id: p.id, schedule: "*/5 * * * *" })
49
- const run = createScanRun(db, { job_id: job.id })
50
- expect(run.status).toBe("running")
51
- expect(run.logs_collected).toBe(0)
52
-
53
- const finished = finishScanRun(db, run.id, { status: "completed", logs_collected: 12, errors_found: 3, perf_score: 87.5 })
54
- expect(finished?.status).toBe("completed")
55
- expect(finished?.logs_collected).toBe(12)
56
- expect(finished?.errors_found).toBe(3)
57
- expect(finished?.perf_score).toBe(87.5)
58
- expect(finished?.finished_at).toBeTruthy()
59
- })
60
-
61
- it("lists scan runs for a job", () => {
62
- const db = createTestDb()
63
- const p = seedProject(db)
64
- const job = createJob(db, { project_id: p.id, schedule: "*/5 * * * *" })
65
- createScanRun(db, { job_id: job.id })
66
- createScanRun(db, { job_id: job.id })
67
- expect(listScanRuns(db, job.id)).toHaveLength(2)
68
- })
69
- })
package/src/lib/jobs.ts DELETED
@@ -1,63 +0,0 @@
1
- import type { Database } from "bun:sqlite"
2
- import type { ScanJob, ScanRun } from "../types/index.ts"
3
-
4
- export function createJob(db: Database, data: { project_id: string; schedule: string; page_id?: string }): ScanJob {
5
- return db.prepare(`
6
- INSERT INTO scan_jobs (project_id, page_id, schedule)
7
- VALUES ($project_id, $page_id, $schedule)
8
- RETURNING *
9
- `).get({
10
- $project_id: data.project_id,
11
- $page_id: data.page_id ?? null,
12
- $schedule: data.schedule,
13
- }) as ScanJob
14
- }
15
-
16
- export function listJobs(db: Database, projectId?: string): ScanJob[] {
17
- if (projectId) {
18
- return db.prepare("SELECT * FROM scan_jobs WHERE project_id = $p ORDER BY created_at DESC").all({ $p: projectId }) as ScanJob[]
19
- }
20
- return db.prepare("SELECT * FROM scan_jobs ORDER BY created_at DESC").all() as ScanJob[]
21
- }
22
-
23
- export function getJob(db: Database, id: string): ScanJob | null {
24
- return db.prepare("SELECT * FROM scan_jobs WHERE id = $id").get({ $id: id }) as ScanJob | null
25
- }
26
-
27
- export function updateJob(db: Database, id: string, data: { enabled?: number; schedule?: string; last_run_at?: string }): ScanJob | null {
28
- const fields = Object.keys(data).map(k => `${k} = $${k}`).join(", ")
29
- if (!fields) return getJob(db, id)
30
- const params = Object.fromEntries(Object.entries(data).map(([k, v]) => [`$${k}`, v]))
31
- params.$id = id
32
- return db.prepare(`UPDATE scan_jobs SET ${fields} WHERE id = $id RETURNING *`).get(params) as ScanJob | null
33
- }
34
-
35
- export function deleteJob(db: Database, id: string): void {
36
- db.run("DELETE FROM scan_jobs WHERE id = $id", { $id: id })
37
- }
38
-
39
- export function createScanRun(db: Database, data: { job_id: string; page_id?: string }): ScanRun {
40
- return db.prepare(`
41
- INSERT INTO scan_runs (job_id, page_id) VALUES ($job_id, $page_id) RETURNING *
42
- `).get({ $job_id: data.job_id, $page_id: data.page_id ?? null }) as ScanRun
43
- }
44
-
45
- export function finishScanRun(db: Database, id: string, data: { status: "completed" | "failed"; logs_collected: number; errors_found: number; perf_score?: number }): ScanRun | null {
46
- return db.prepare(`
47
- UPDATE scan_runs SET finished_at = strftime('%Y-%m-%dT%H:%M:%fZ','now'),
48
- status = $status, logs_collected = $logs_collected,
49
- errors_found = $errors_found, perf_score = $perf_score
50
- WHERE id = $id RETURNING *
51
- `).get({
52
- $id: id,
53
- $status: data.status,
54
- $logs_collected: data.logs_collected,
55
- $errors_found: data.errors_found,
56
- $perf_score: data.perf_score ?? null,
57
- }) as ScanRun | null
58
- }
59
-
60
- export function listScanRuns(db: Database, jobId: string, limit = 20): ScanRun[] {
61
- return db.prepare("SELECT * FROM scan_runs WHERE job_id = $j ORDER BY started_at DESC LIMIT $l")
62
- .all({ $j: jobId, $l: limit }) as ScanRun[]
63
- }
@@ -1,65 +0,0 @@
1
- import type { Database } from "bun:sqlite"
2
- import { saveSnapshot } from "./perf.ts"
3
- import type { PerformanceSnapshot } from "../types/index.ts"
4
-
5
- export interface LighthouseResult {
6
- lcp: number | null
7
- fcp: number | null
8
- cls: number | null
9
- tti: number | null
10
- ttfb: number | null
11
- score: number | null
12
- raw_audit: string
13
- }
14
-
15
- export async function runLighthouse(url: string): Promise<LighthouseResult | null> {
16
- try {
17
- // Dynamic import — lighthouse is an optional peer dep
18
- const { default: lighthouse } = await import("lighthouse" as string)
19
- const { chromium } = await import("playwright")
20
-
21
- const browser = await chromium.launch({ headless: true, args: ["--remote-debugging-port=9222"] })
22
- try {
23
- const result = await lighthouse(url, {
24
- port: 9222,
25
- output: "json",
26
- logLevel: "silent",
27
- onlyCategories: ["performance"],
28
- } as Parameters<typeof lighthouse>[1])
29
-
30
- if (!result) return null
31
- const audits = result.lhr.audits
32
- const score = result.lhr.categories["performance"]?.score
33
-
34
- return {
35
- lcp: (audits["largest-contentful-paint"]?.numericValue ?? null) as number | null,
36
- fcp: (audits["first-contentful-paint"]?.numericValue ?? null) as number | null,
37
- cls: (audits["cumulative-layout-shift"]?.numericValue ?? null) as number | null,
38
- tti: (audits["interactive"]?.numericValue ?? null) as number | null,
39
- ttfb: (audits["server-response-time"]?.numericValue ?? null) as number | null,
40
- score: score !== undefined ? score * 100 : null,
41
- raw_audit: JSON.stringify(result.lhr.audits),
42
- }
43
- } finally {
44
- await browser.close()
45
- }
46
- } catch {
47
- return null
48
- }
49
- }
50
-
51
- export async function runAndSaveLighthouse(
52
- db: Database,
53
- url: string,
54
- projectId: string,
55
- pageId?: string,
56
- ): Promise<PerformanceSnapshot | null> {
57
- const result = await runLighthouse(url)
58
- if (!result) return null
59
- return saveSnapshot(db, {
60
- project_id: projectId,
61
- page_id: pageId ?? null,
62
- url,
63
- ...result,
64
- })
65
- }
@@ -1,43 +0,0 @@
1
- import { afterEach, expect, test } from "bun:test"
2
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
3
- import { tmpdir } from "node:os"
4
- import { join } from "node:path"
5
- import { pathToFileURL } from "node:url"
6
- import { readPackageVersion } from "./package-meta.ts"
7
-
8
- const tempRoots: string[] = []
9
-
10
- function createFixture() {
11
- const root = mkdtempSync(join(tmpdir(), "logs-package-meta-"))
12
- tempRoots.push(root)
13
-
14
- mkdirSync(join(root, "src/lib"), { recursive: true })
15
- mkdirSync(join(root, "dist"), { recursive: true })
16
- writeFileSync(join(root, "package.json"), JSON.stringify({ version: "9.9.9" }), "utf8")
17
-
18
- return root
19
- }
20
-
21
- afterEach(() => {
22
- while (tempRoots.length > 0) {
23
- const root = tempRoots.pop()
24
- if (!root) continue
25
- rmSync(root, { recursive: true, force: true })
26
- }
27
- })
28
-
29
- test("readPackageVersion resolves package.json from source module paths", () => {
30
- const root = createFixture()
31
-
32
- const version = readPackageVersion(pathToFileURL(join(root, "src/lib/fake.js")))
33
-
34
- expect(version).toBe("9.9.9")
35
- })
36
-
37
- test("readPackageVersion resolves package.json from bundled dist chunk paths", () => {
38
- const root = createFixture()
39
-
40
- const version = readPackageVersion(pathToFileURL(join(root, "dist/index-abc123.js")))
41
-
42
- expect(version).toBe("9.9.9")
43
- })
@@ -1,80 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs"
2
-
3
- type StandaloneCliSpec = {
4
- name: string
5
- description: string
6
- options?: string[]
7
- }
8
-
9
- type PackageJson = {
10
- version?: string
11
- }
12
-
13
- const PACKAGE_JSON_CANDIDATES = [
14
- "../../package.json",
15
- "../package.json",
16
- "./package.json",
17
- ]
18
-
19
- function readPackageJson(baseUrl: string | URL = import.meta.url): PackageJson {
20
- // Bundled shared chunks live under dist/, while source modules live under src/lib/.
21
- for (const relativePath of PACKAGE_JSON_CANDIDATES) {
22
- const candidate = new URL(relativePath, baseUrl)
23
- if (!existsSync(candidate)) continue
24
-
25
- return JSON.parse(readFileSync(candidate, "utf8")) as PackageJson
26
- }
27
-
28
- throw new Error(`Unable to locate package.json from ${String(baseUrl)}`)
29
- }
30
-
31
- export function readPackageVersion(baseUrl: string | URL = import.meta.url): string {
32
- return readPackageJson(baseUrl).version ?? "0.0.0"
33
- }
34
-
35
- export const PACKAGE_VERSION = readPackageVersion()
36
-
37
- export function exitIfMetadataRequest(spec: StandaloneCliSpec, argv = process.argv.slice(2)): void {
38
- if (argv.includes("--version") || argv.includes("-V")) {
39
- console.log(PACKAGE_VERSION)
40
- process.exit(0)
41
- }
42
-
43
- if (argv.includes("--help") || argv.includes("-h")) {
44
- const options = spec.options ?? []
45
- const renderedOptions = [
46
- " -V, --version output the version number",
47
- " -h, --help display help for command",
48
- ...options,
49
- ]
50
-
51
- console.log(
52
- [
53
- `Usage: ${spec.name} [options]`,
54
- "",
55
- spec.description,
56
- "",
57
- "Options:",
58
- ...renderedOptions,
59
- ].join("\n"),
60
- )
61
- process.exit(0)
62
- }
63
- }
64
-
65
- export function readOptionValue(names: string[], argv = process.argv.slice(2)): string | undefined {
66
- for (let index = 0; index < argv.length; index += 1) {
67
- const arg = argv[index]
68
- if (!arg) continue
69
-
70
- const inline = names.find((name) => arg.startsWith(`${name}=`))
71
- if (inline) return arg.slice(inline.length + 1)
72
-
73
- if (names.includes(arg)) {
74
- const next = argv[index + 1]
75
- if (next && !next.startsWith("-")) return next
76
- }
77
- }
78
-
79
- return undefined
80
- }
@@ -1,54 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { createTestDb } from "../db/index.ts"
3
- import { deletePageAuth, getPageAuth, setPageAuth } from "./page-auth.ts"
4
-
5
- function seedPage(db: ReturnType<typeof createTestDb>) {
6
- const p = db.prepare("INSERT INTO projects (name) VALUES ('app') RETURNING id").get() as { id: string }
7
- const page = db.prepare("INSERT INTO pages (project_id, url) VALUES (?, 'https://app.com') RETURNING id").get(p.id) as { id: string }
8
- return { projectId: p.id, pageId: page.id }
9
- }
10
-
11
- describe("page auth", () => {
12
- it("sets and retrieves bearer auth", () => {
13
- const db = createTestDb()
14
- const { pageId } = seedPage(db)
15
- setPageAuth(db, pageId, "bearer", "my-token-123")
16
- const auth = getPageAuth(db, pageId)
17
- expect(auth?.type).toBe("bearer")
18
- expect(auth?.credentials).toBe("my-token-123")
19
- })
20
-
21
- it("credentials are encrypted at rest", () => {
22
- const db = createTestDb()
23
- const { pageId } = seedPage(db)
24
- setPageAuth(db, pageId, "bearer", "secret-token")
25
- const raw = db.prepare("SELECT credentials FROM page_auth WHERE page_id = ?").get(pageId) as { credentials: string }
26
- // Raw value should NOT be the plaintext token
27
- expect(raw.credentials).not.toBe("secret-token")
28
- expect(raw.credentials).toContain(":") // IV:encrypted format
29
- })
30
-
31
- it("upserts on duplicate page_id", () => {
32
- const db = createTestDb()
33
- const { pageId } = seedPage(db)
34
- setPageAuth(db, pageId, "bearer", "token-v1")
35
- setPageAuth(db, pageId, "bearer", "token-v2")
36
- const auth = getPageAuth(db, pageId)
37
- expect(auth?.credentials).toBe("token-v2")
38
- const { c } = db.prepare("SELECT COUNT(*) as c FROM page_auth WHERE page_id = ?").get(pageId) as { c: number }
39
- expect(c).toBe(1)
40
- })
41
-
42
- it("returns null for unknown page", () => {
43
- const db = createTestDb()
44
- expect(getPageAuth(db, "nope")).toBeNull()
45
- })
46
-
47
- it("deletes auth", () => {
48
- const db = createTestDb()
49
- const { pageId } = seedPage(db)
50
- setPageAuth(db, pageId, "basic", "user:pass")
51
- deletePageAuth(db, pageId)
52
- expect(getPageAuth(db, pageId)).toBeNull()
53
- })
54
- })