@hasna/logs 0.0.1 → 0.2.0

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 (65) hide show
  1. package/dashboard/README.md +73 -0
  2. package/dashboard/bun.lock +526 -0
  3. package/dashboard/eslint.config.js +23 -0
  4. package/dashboard/index.html +13 -0
  5. package/dashboard/package.json +32 -0
  6. package/dashboard/public/favicon.svg +1 -0
  7. package/dashboard/public/icons.svg +24 -0
  8. package/dashboard/src/App.css +184 -0
  9. package/dashboard/src/App.tsx +49 -0
  10. package/dashboard/src/api.ts +33 -0
  11. package/dashboard/src/assets/hero.png +0 -0
  12. package/dashboard/src/assets/react.svg +1 -0
  13. package/dashboard/src/assets/vite.svg +1 -0
  14. package/dashboard/src/index.css +111 -0
  15. package/dashboard/src/main.tsx +10 -0
  16. package/dashboard/src/pages/Alerts.tsx +69 -0
  17. package/dashboard/src/pages/Issues.tsx +50 -0
  18. package/dashboard/src/pages/Perf.tsx +75 -0
  19. package/dashboard/src/pages/Projects.tsx +67 -0
  20. package/dashboard/src/pages/Summary.tsx +67 -0
  21. package/dashboard/src/pages/Tail.tsx +65 -0
  22. package/dashboard/tsconfig.app.json +28 -0
  23. package/dashboard/tsconfig.json +7 -0
  24. package/dashboard/tsconfig.node.json +26 -0
  25. package/dashboard/vite.config.ts +14 -0
  26. package/dist/cli/index.js +116 -12
  27. package/dist/mcp/index.js +306 -100
  28. package/dist/server/index.js +592 -7
  29. package/package.json +12 -2
  30. package/sdk/package.json +3 -2
  31. package/sdk/src/index.ts +1 -1
  32. package/sdk/src/types.ts +56 -0
  33. package/src/cli/index.ts +114 -4
  34. package/src/db/index.ts +10 -0
  35. package/src/db/migrations/001_alert_rules.ts +21 -0
  36. package/src/db/migrations/002_issues.ts +21 -0
  37. package/src/db/migrations/003_retention.ts +15 -0
  38. package/src/db/migrations/004_page_auth.ts +13 -0
  39. package/src/lib/alerts.test.ts +67 -0
  40. package/src/lib/alerts.ts +117 -0
  41. package/src/lib/compare.test.ts +52 -0
  42. package/src/lib/compare.ts +85 -0
  43. package/src/lib/diagnose.test.ts +55 -0
  44. package/src/lib/diagnose.ts +76 -0
  45. package/src/lib/export.test.ts +66 -0
  46. package/src/lib/export.ts +65 -0
  47. package/src/lib/health.test.ts +48 -0
  48. package/src/lib/health.ts +51 -0
  49. package/src/lib/ingest.ts +25 -2
  50. package/src/lib/issues.test.ts +79 -0
  51. package/src/lib/issues.ts +70 -0
  52. package/src/lib/page-auth.test.ts +54 -0
  53. package/src/lib/page-auth.ts +48 -0
  54. package/src/lib/retention.test.ts +42 -0
  55. package/src/lib/retention.ts +62 -0
  56. package/src/lib/scanner.ts +21 -2
  57. package/src/lib/scheduler.ts +6 -0
  58. package/src/lib/session-context.ts +28 -0
  59. package/src/mcp/index.ts +133 -89
  60. package/src/server/index.ts +12 -1
  61. package/src/server/routes/alerts.ts +32 -0
  62. package/src/server/routes/issues.ts +43 -0
  63. package/src/server/routes/logs.ts +21 -0
  64. package/src/server/routes/projects.ts +25 -0
  65. package/src/server/routes/stream.ts +43 -0
@@ -1,31 +1,52 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
  import {
4
+ deletePageAuth,
5
+ runRetentionForProject,
6
+ setPageAuth,
7
+ setRetentionPolicy,
4
8
  startScheduler
5
- } from "../index-dh02dp7n.js";
9
+ } from "../index-2p6ynjet.js";
6
10
  import {
11
+ createAlertRule,
7
12
  createPage,
8
13
  createProject,
14
+ deleteAlertRule,
9
15
  getDb,
16
+ getIssue,
10
17
  getLatestSnapshot,
11
- getLogContext,
12
18
  getPerfTrend,
13
19
  getProject,
14
20
  ingestBatch,
15
21
  ingestLog,
22
+ listAlertRules,
23
+ listIssues,
16
24
  listPages,
17
25
  listProjects,
18
- searchLogs,
19
26
  summarizeLogs,
20
- tailLogs,
27
+ updateAlertRule,
28
+ updateIssueStatus,
21
29
  updateProject
22
- } from "../index-zj6ymcv7.js";
30
+ } from "../index-77ss2sf4.js";
23
31
  import {
24
32
  createJob,
25
33
  deleteJob,
26
34
  listJobs,
27
35
  updateJob
28
- } from "../index-4mnved04.js";
36
+ } from "../jobs-124e878j.js";
37
+ import {
38
+ getLogContext,
39
+ searchLogs,
40
+ tailLogs
41
+ } from "../query-0qv7fvzt.js";
42
+ import {
43
+ exportToCsv,
44
+ exportToJson
45
+ } from "../export-yjaw2sr3.js";
46
+ import {
47
+ getHealth
48
+ } from "../health-f2qrebqc.js";
49
+ import"../index-g8dczzvv.js";
29
50
 
30
51
  // node_modules/hono/dist/compose.js
31
52
  var compose = (middleware, onError, onNotFound) => {
@@ -1650,6 +1671,277 @@ var cors = (options) => {
1650
1671
  };
1651
1672
  };
1652
1673
 
1674
+ // node_modules/hono/dist/adapter/bun/serve-static.js
1675
+ import { stat } from "fs/promises";
1676
+ import { join } from "path";
1677
+
1678
+ // node_modules/hono/dist/utils/compress.js
1679
+ var COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/(?!event-stream(?:[;\s]|$))[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i;
1680
+
1681
+ // node_modules/hono/dist/utils/mime.js
1682
+ var getMimeType = (filename, mimes = baseMimes) => {
1683
+ const regexp = /\.([a-zA-Z0-9]+?)$/;
1684
+ const match2 = filename.match(regexp);
1685
+ if (!match2) {
1686
+ return;
1687
+ }
1688
+ let mimeType = mimes[match2[1].toLowerCase()];
1689
+ if (mimeType && mimeType.startsWith("text")) {
1690
+ mimeType += "; charset=utf-8";
1691
+ }
1692
+ return mimeType;
1693
+ };
1694
+ var _baseMimes = {
1695
+ aac: "audio/aac",
1696
+ avi: "video/x-msvideo",
1697
+ avif: "image/avif",
1698
+ av1: "video/av1",
1699
+ bin: "application/octet-stream",
1700
+ bmp: "image/bmp",
1701
+ css: "text/css",
1702
+ csv: "text/csv",
1703
+ eot: "application/vnd.ms-fontobject",
1704
+ epub: "application/epub+zip",
1705
+ gif: "image/gif",
1706
+ gz: "application/gzip",
1707
+ htm: "text/html",
1708
+ html: "text/html",
1709
+ ico: "image/x-icon",
1710
+ ics: "text/calendar",
1711
+ jpeg: "image/jpeg",
1712
+ jpg: "image/jpeg",
1713
+ js: "text/javascript",
1714
+ json: "application/json",
1715
+ jsonld: "application/ld+json",
1716
+ map: "application/json",
1717
+ mid: "audio/x-midi",
1718
+ midi: "audio/x-midi",
1719
+ mjs: "text/javascript",
1720
+ mp3: "audio/mpeg",
1721
+ mp4: "video/mp4",
1722
+ mpeg: "video/mpeg",
1723
+ oga: "audio/ogg",
1724
+ ogv: "video/ogg",
1725
+ ogx: "application/ogg",
1726
+ opus: "audio/opus",
1727
+ otf: "font/otf",
1728
+ pdf: "application/pdf",
1729
+ png: "image/png",
1730
+ rtf: "application/rtf",
1731
+ svg: "image/svg+xml",
1732
+ tif: "image/tiff",
1733
+ tiff: "image/tiff",
1734
+ ts: "video/mp2t",
1735
+ ttf: "font/ttf",
1736
+ txt: "text/plain",
1737
+ wasm: "application/wasm",
1738
+ webm: "video/webm",
1739
+ weba: "audio/webm",
1740
+ webmanifest: "application/manifest+json",
1741
+ webp: "image/webp",
1742
+ woff: "font/woff",
1743
+ woff2: "font/woff2",
1744
+ xhtml: "application/xhtml+xml",
1745
+ xml: "application/xml",
1746
+ zip: "application/zip",
1747
+ "3gp": "video/3gpp",
1748
+ "3g2": "video/3gpp2",
1749
+ gltf: "model/gltf+json",
1750
+ glb: "model/gltf-binary"
1751
+ };
1752
+ var baseMimes = _baseMimes;
1753
+
1754
+ // node_modules/hono/dist/middleware/serve-static/path.js
1755
+ var defaultJoin = (...paths) => {
1756
+ let result = paths.filter((p) => p !== "").join("/");
1757
+ result = result.replace(/(?<=\/)\/+/g, "");
1758
+ const segments = result.split("/");
1759
+ const resolved = [];
1760
+ for (const segment of segments) {
1761
+ if (segment === ".." && resolved.length > 0 && resolved.at(-1) !== "..") {
1762
+ resolved.pop();
1763
+ } else if (segment !== ".") {
1764
+ resolved.push(segment);
1765
+ }
1766
+ }
1767
+ return resolved.join("/") || ".";
1768
+ };
1769
+
1770
+ // node_modules/hono/dist/middleware/serve-static/index.js
1771
+ var ENCODINGS = {
1772
+ br: ".br",
1773
+ zstd: ".zst",
1774
+ gzip: ".gz"
1775
+ };
1776
+ var ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS);
1777
+ var DEFAULT_DOCUMENT = "index.html";
1778
+ var serveStatic = (options) => {
1779
+ const root = options.root ?? "./";
1780
+ const optionPath = options.path;
1781
+ const join = options.join ?? defaultJoin;
1782
+ return async (c, next) => {
1783
+ if (c.finalized) {
1784
+ return next();
1785
+ }
1786
+ let filename;
1787
+ if (options.path) {
1788
+ filename = options.path;
1789
+ } else {
1790
+ try {
1791
+ filename = tryDecodeURI(c.req.path);
1792
+ if (/(?:^|[\/\\])\.\.(?:$|[\/\\])/.test(filename)) {
1793
+ throw new Error;
1794
+ }
1795
+ } catch {
1796
+ await options.onNotFound?.(c.req.path, c);
1797
+ return next();
1798
+ }
1799
+ }
1800
+ let path = join(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename) : filename);
1801
+ if (options.isDir && await options.isDir(path)) {
1802
+ path = join(path, DEFAULT_DOCUMENT);
1803
+ }
1804
+ const getContent = options.getContent;
1805
+ let content = await getContent(path, c);
1806
+ if (content instanceof Response) {
1807
+ return c.newResponse(content.body, content);
1808
+ }
1809
+ if (content) {
1810
+ const mimeType = options.mimes && getMimeType(path, options.mimes) || getMimeType(path);
1811
+ c.header("Content-Type", mimeType || "application/octet-stream");
1812
+ if (options.precompressed && (!mimeType || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) {
1813
+ const acceptEncodingSet = new Set(c.req.header("Accept-Encoding")?.split(",").map((encoding) => encoding.trim()));
1814
+ for (const encoding of ENCODINGS_ORDERED_KEYS) {
1815
+ if (!acceptEncodingSet.has(encoding)) {
1816
+ continue;
1817
+ }
1818
+ const compressedContent = await getContent(path + ENCODINGS[encoding], c);
1819
+ if (compressedContent) {
1820
+ content = compressedContent;
1821
+ c.header("Content-Encoding", encoding);
1822
+ c.header("Vary", "Accept-Encoding", { append: true });
1823
+ break;
1824
+ }
1825
+ }
1826
+ }
1827
+ await options.onFound?.(path, c);
1828
+ return c.body(content);
1829
+ }
1830
+ await options.onNotFound?.(path, c);
1831
+ await next();
1832
+ return;
1833
+ };
1834
+ };
1835
+
1836
+ // node_modules/hono/dist/adapter/bun/serve-static.js
1837
+ var serveStatic2 = (options) => {
1838
+ return async function serveStatic2(c, next) {
1839
+ const getContent = async (path) => {
1840
+ const file = Bun.file(path);
1841
+ return await file.exists() ? file : null;
1842
+ };
1843
+ const isDir = async (path) => {
1844
+ let isDir2;
1845
+ try {
1846
+ const stats = await stat(path);
1847
+ isDir2 = stats.isDirectory();
1848
+ } catch {}
1849
+ return isDir2;
1850
+ };
1851
+ return serveStatic({
1852
+ ...options,
1853
+ getContent,
1854
+ join,
1855
+ isDir
1856
+ })(c, next);
1857
+ };
1858
+ };
1859
+
1860
+ // node_modules/hono/dist/helper/ssg/middleware.js
1861
+ var X_HONO_DISABLE_SSG_HEADER_KEY = "x-hono-disable-ssg";
1862
+ var SSG_DISABLED_RESPONSE = (() => {
1863
+ try {
1864
+ return new Response("SSG is disabled", {
1865
+ status: 404,
1866
+ headers: { [X_HONO_DISABLE_SSG_HEADER_KEY]: "true" }
1867
+ });
1868
+ } catch {
1869
+ return null;
1870
+ }
1871
+ })();
1872
+ // node_modules/hono/dist/adapter/bun/ssg.js
1873
+ var { write } = Bun;
1874
+
1875
+ // node_modules/hono/dist/helper/websocket/index.js
1876
+ var WSContext = class {
1877
+ #init;
1878
+ constructor(init) {
1879
+ this.#init = init;
1880
+ this.raw = init.raw;
1881
+ this.url = init.url ? new URL(init.url) : null;
1882
+ this.protocol = init.protocol ?? null;
1883
+ }
1884
+ send(source, options) {
1885
+ this.#init.send(source, options ?? {});
1886
+ }
1887
+ raw;
1888
+ binaryType = "arraybuffer";
1889
+ get readyState() {
1890
+ return this.#init.readyState;
1891
+ }
1892
+ url;
1893
+ protocol;
1894
+ close(code, reason) {
1895
+ this.#init.close(code, reason);
1896
+ }
1897
+ };
1898
+ var defineWebSocketHelper = (handler) => {
1899
+ return (...args) => {
1900
+ if (typeof args[0] === "function") {
1901
+ const [createEvents, options] = args;
1902
+ return async function upgradeWebSocket(c, next) {
1903
+ const events = await createEvents(c);
1904
+ const result = await handler(c, events, options);
1905
+ if (result) {
1906
+ return result;
1907
+ }
1908
+ await next();
1909
+ };
1910
+ } else {
1911
+ const [c, events, options] = args;
1912
+ return (async () => {
1913
+ const upgraded = await handler(c, events, options);
1914
+ if (!upgraded) {
1915
+ throw new Error("Failed to upgrade WebSocket");
1916
+ }
1917
+ return upgraded;
1918
+ })();
1919
+ }
1920
+ };
1921
+ };
1922
+
1923
+ // node_modules/hono/dist/adapter/bun/server.js
1924
+ var getBunServer = (c) => ("server" in c.env) ? c.env.server : c.env;
1925
+
1926
+ // node_modules/hono/dist/adapter/bun/websocket.js
1927
+ var upgradeWebSocket = defineWebSocketHelper((c, events) => {
1928
+ const server = getBunServer(c);
1929
+ if (!server) {
1930
+ throw new TypeError("env has to include the 2nd argument of fetch.");
1931
+ }
1932
+ const upgradeResult = server.upgrade(c.req.raw, {
1933
+ data: {
1934
+ events,
1935
+ url: new URL(c.req.url),
1936
+ protocol: c.req.url
1937
+ }
1938
+ });
1939
+ if (upgradeResult) {
1940
+ return new Response(null);
1941
+ }
1942
+ return;
1943
+ });
1944
+
1653
1945
  // src/lib/browser-script.ts
1654
1946
  function getBrowserScript(serverUrl) {
1655
1947
  return `(function(){
@@ -1674,6 +1966,71 @@ window.__logs={push:push,flush:flush,config:cfg};
1674
1966
  })();`;
1675
1967
  }
1676
1968
 
1969
+ // src/server/routes/alerts.ts
1970
+ function alertsRoutes(db) {
1971
+ const app = new Hono2;
1972
+ app.post("/", async (c) => {
1973
+ const body = await c.req.json();
1974
+ if (!body.project_id || !body.name)
1975
+ return c.json({ error: "project_id and name required" }, 422);
1976
+ return c.json(createAlertRule(db, body), 201);
1977
+ });
1978
+ app.get("/", (c) => {
1979
+ const { project_id } = c.req.query();
1980
+ return c.json(listAlertRules(db, project_id || undefined));
1981
+ });
1982
+ app.put("/:id", async (c) => {
1983
+ const body = await c.req.json();
1984
+ const updated = updateAlertRule(db, c.req.param("id"), body);
1985
+ if (!updated)
1986
+ return c.json({ error: "not found" }, 404);
1987
+ return c.json(updated);
1988
+ });
1989
+ app.delete("/:id", (c) => {
1990
+ deleteAlertRule(db, c.req.param("id"));
1991
+ return c.json({ deleted: true });
1992
+ });
1993
+ return app;
1994
+ }
1995
+
1996
+ // src/server/routes/issues.ts
1997
+ function issuesRoutes(db) {
1998
+ const app = new Hono2;
1999
+ app.get("/", (c) => {
2000
+ const { project_id, status, limit } = c.req.query();
2001
+ return c.json(listIssues(db, project_id || undefined, status || undefined, limit ? Number(limit) : 50));
2002
+ });
2003
+ app.get("/:id", (c) => {
2004
+ const issue = getIssue(db, c.req.param("id"));
2005
+ if (!issue)
2006
+ return c.json({ error: "not found" }, 404);
2007
+ return c.json(issue);
2008
+ });
2009
+ app.get("/:id/logs", (c) => {
2010
+ const issue = getIssue(db, c.req.param("id"));
2011
+ if (!issue)
2012
+ return c.json({ error: "not found" }, 404);
2013
+ const rows = searchLogs(db, {
2014
+ project_id: issue.project_id ?? undefined,
2015
+ level: issue.level,
2016
+ service: issue.service ?? undefined,
2017
+ text: issue.message_template.slice(0, 50),
2018
+ limit: 50
2019
+ });
2020
+ return c.json(rows);
2021
+ });
2022
+ app.put("/:id", async (c) => {
2023
+ const { status } = await c.req.json();
2024
+ if (!["open", "resolved", "ignored"].includes(status))
2025
+ return c.json({ error: "invalid status" }, 422);
2026
+ const updated = updateIssueStatus(db, c.req.param("id"), status);
2027
+ if (!updated)
2028
+ return c.json({ error: "not found" }, 404);
2029
+ return c.json(updated);
2030
+ });
2031
+ return app;
2032
+ }
2033
+
1677
2034
  // src/server/routes/jobs.ts
1678
2035
  function jobsRoutes(db) {
1679
2036
  const app = new Hono2;
@@ -1747,6 +2104,23 @@ function logsRoutes(db) {
1747
2104
  const rows = getLogContext(db, c.req.param("trace_id"));
1748
2105
  return c.json(rows);
1749
2106
  });
2107
+ app.get("/export", (c) => {
2108
+ const { project_id, since, until, level, service, format, limit } = c.req.query();
2109
+ const opts = { project_id: project_id || undefined, since: since || undefined, until: until || undefined, level: level || undefined, service: service || undefined, limit: limit ? Number(limit) : undefined };
2110
+ if (format === "csv") {
2111
+ c.header("Content-Type", "text/csv");
2112
+ c.header("Content-Disposition", "attachment; filename=logs.csv");
2113
+ const chunks2 = [];
2114
+ exportToCsv(db, opts, (s) => chunks2.push(s));
2115
+ return c.text(chunks2.join(""));
2116
+ }
2117
+ c.header("Content-Type", "application/json");
2118
+ c.header("Content-Disposition", "attachment; filename=logs.json");
2119
+ const chunks = [];
2120
+ exportToJson(db, opts, (s) => chunks.push(s));
2121
+ return c.text(chunks.join(`
2122
+ `));
2123
+ });
1750
2124
  return app;
1751
2125
  }
1752
2126
 
@@ -1823,6 +2197,26 @@ function projectsRoutes(db) {
1823
2197
  return c.json(page, 201);
1824
2198
  });
1825
2199
  app.get("/:id/pages", (c) => c.json(listPages(db, c.req.param("id"))));
2200
+ app.put("/:id/retention", async (c) => {
2201
+ const body = await c.req.json();
2202
+ setRetentionPolicy(db, c.req.param("id"), body);
2203
+ return c.json({ updated: true });
2204
+ });
2205
+ app.post("/:id/retention/run", (c) => {
2206
+ const result = runRetentionForProject(db, c.req.param("id"));
2207
+ return c.json(result);
2208
+ });
2209
+ app.post("/:id/pages/:page_id/auth", async (c) => {
2210
+ const { type, credentials } = await c.req.json();
2211
+ if (!type || !credentials)
2212
+ return c.json({ error: "type and credentials required" }, 422);
2213
+ const result = setPageAuth(db, c.req.param("page_id"), type, credentials);
2214
+ return c.json({ id: result.id, type: result.type, created_at: result.created_at }, 201);
2215
+ });
2216
+ app.delete("/:id/pages/:page_id/auth", (c) => {
2217
+ deletePageAuth(db, c.req.param("page_id"));
2218
+ return c.json({ deleted: true });
2219
+ });
1826
2220
  app.post("/:id/sync-repo", async (c) => {
1827
2221
  const project = getProject(db, c.req.param("id"));
1828
2222
  if (!project)
@@ -1835,6 +2229,191 @@ function projectsRoutes(db) {
1835
2229
  return app;
1836
2230
  }
1837
2231
 
2232
+ // node_modules/hono/dist/utils/stream.js
2233
+ var StreamingApi = class {
2234
+ writer;
2235
+ encoder;
2236
+ writable;
2237
+ abortSubscribers = [];
2238
+ responseReadable;
2239
+ aborted = false;
2240
+ closed = false;
2241
+ constructor(writable, _readable) {
2242
+ this.writable = writable;
2243
+ this.writer = writable.getWriter();
2244
+ this.encoder = new TextEncoder;
2245
+ const reader = _readable.getReader();
2246
+ this.abortSubscribers.push(async () => {
2247
+ await reader.cancel();
2248
+ });
2249
+ this.responseReadable = new ReadableStream({
2250
+ async pull(controller) {
2251
+ const { done, value } = await reader.read();
2252
+ done ? controller.close() : controller.enqueue(value);
2253
+ },
2254
+ cancel: () => {
2255
+ this.abort();
2256
+ }
2257
+ });
2258
+ }
2259
+ async write(input) {
2260
+ try {
2261
+ if (typeof input === "string") {
2262
+ input = this.encoder.encode(input);
2263
+ }
2264
+ await this.writer.write(input);
2265
+ } catch {}
2266
+ return this;
2267
+ }
2268
+ async writeln(input) {
2269
+ await this.write(input + `
2270
+ `);
2271
+ return this;
2272
+ }
2273
+ sleep(ms) {
2274
+ return new Promise((res) => setTimeout(res, ms));
2275
+ }
2276
+ async close() {
2277
+ try {
2278
+ await this.writer.close();
2279
+ } catch {}
2280
+ this.closed = true;
2281
+ }
2282
+ async pipe(body) {
2283
+ this.writer.releaseLock();
2284
+ await body.pipeTo(this.writable, { preventClose: true });
2285
+ this.writer = this.writable.getWriter();
2286
+ }
2287
+ onAbort(listener) {
2288
+ this.abortSubscribers.push(listener);
2289
+ }
2290
+ abort() {
2291
+ if (!this.aborted) {
2292
+ this.aborted = true;
2293
+ this.abortSubscribers.forEach((subscriber) => subscriber());
2294
+ }
2295
+ }
2296
+ };
2297
+
2298
+ // node_modules/hono/dist/helper/streaming/utils.js
2299
+ var isOldBunVersion = () => {
2300
+ const version = typeof Bun !== "undefined" ? Bun.version : undefined;
2301
+ if (version === undefined) {
2302
+ return false;
2303
+ }
2304
+ const result = version.startsWith("1.1") || version.startsWith("1.0") || version.startsWith("0.");
2305
+ isOldBunVersion = () => result;
2306
+ return result;
2307
+ };
2308
+
2309
+ // node_modules/hono/dist/helper/streaming/sse.js
2310
+ var SSEStreamingApi = class extends StreamingApi {
2311
+ constructor(writable, readable) {
2312
+ super(writable, readable);
2313
+ }
2314
+ async writeSSE(message) {
2315
+ const data = await resolveCallback(message.data, HtmlEscapedCallbackPhase.Stringify, false, {});
2316
+ const dataLines = data.split(/\r\n|\r|\n/).map((line) => {
2317
+ return `data: ${line}`;
2318
+ }).join(`
2319
+ `);
2320
+ for (const key of ["event", "id", "retry"]) {
2321
+ if (message[key] && /[\r\n]/.test(message[key])) {
2322
+ throw new Error(`${key} must not contain "\\r" or "\\n"`);
2323
+ }
2324
+ }
2325
+ const sseData = [
2326
+ message.event && `event: ${message.event}`,
2327
+ dataLines,
2328
+ message.id && `id: ${message.id}`,
2329
+ message.retry && `retry: ${message.retry}`
2330
+ ].filter(Boolean).join(`
2331
+ `) + `
2332
+
2333
+ `;
2334
+ await this.write(sseData);
2335
+ }
2336
+ };
2337
+ var run = async (stream, cb, onError) => {
2338
+ try {
2339
+ await cb(stream);
2340
+ } catch (e) {
2341
+ if (e instanceof Error && onError) {
2342
+ await onError(e, stream);
2343
+ await stream.writeSSE({
2344
+ event: "error",
2345
+ data: e.message
2346
+ });
2347
+ } else {
2348
+ console.error(e);
2349
+ }
2350
+ } finally {
2351
+ stream.close();
2352
+ }
2353
+ };
2354
+ var contextStash = /* @__PURE__ */ new WeakMap;
2355
+ var streamSSE = (c, cb, onError) => {
2356
+ const { readable, writable } = new TransformStream;
2357
+ const stream = new SSEStreamingApi(writable, readable);
2358
+ if (isOldBunVersion()) {
2359
+ c.req.raw.signal.addEventListener("abort", () => {
2360
+ if (!stream.closed) {
2361
+ stream.abort();
2362
+ }
2363
+ });
2364
+ }
2365
+ contextStash.set(stream.responseReadable, c);
2366
+ c.header("Transfer-Encoding", "chunked");
2367
+ c.header("Content-Type", "text/event-stream");
2368
+ c.header("Cache-Control", "no-cache");
2369
+ c.header("Connection", "keep-alive");
2370
+ run(stream, cb, onError);
2371
+ return c.newResponse(stream.responseReadable);
2372
+ };
2373
+
2374
+ // src/server/routes/stream.ts
2375
+ function streamRoutes(db) {
2376
+ const app = new Hono2;
2377
+ app.get("/", (c) => {
2378
+ const { project_id, level, service } = c.req.query();
2379
+ return streamSSE(c, async (stream2) => {
2380
+ let lastId = null;
2381
+ const latest = db.prepare("SELECT id FROM logs ORDER BY timestamp DESC LIMIT 1").get();
2382
+ lastId = latest?.id ?? null;
2383
+ while (true) {
2384
+ const conditions = [];
2385
+ const params = {};
2386
+ if (lastId) {
2387
+ conditions.push("rowid > (SELECT rowid FROM logs WHERE id = $lastId)");
2388
+ params.$lastId = lastId;
2389
+ }
2390
+ if (project_id) {
2391
+ conditions.push("project_id = $project_id");
2392
+ params.$project_id = project_id;
2393
+ }
2394
+ if (level) {
2395
+ conditions.push("level IN (" + level.split(",").map((l, i) => `$l${i}`).join(",") + ")");
2396
+ level.split(",").forEach((l, i) => {
2397
+ params[`$l${i}`] = l;
2398
+ });
2399
+ }
2400
+ if (service) {
2401
+ conditions.push("service = $service");
2402
+ params.$service = service;
2403
+ }
2404
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
2405
+ const rows = db.prepare(`SELECT * FROM logs ${where} ORDER BY timestamp ASC LIMIT 50`).all(params);
2406
+ for (const row of rows) {
2407
+ await stream2.writeSSE({ data: JSON.stringify(row), id: row.id, event: row.level });
2408
+ lastId = row.id;
2409
+ }
2410
+ await stream2.sleep(500);
2411
+ }
2412
+ });
2413
+ });
2414
+ return app;
2415
+ }
2416
+
1838
2417
  // src/server/index.ts
1839
2418
  var PORT = Number(process.env.LOGS_PORT ?? 3460);
1840
2419
  var db = getDb();
@@ -1847,10 +2426,16 @@ app.get("/script.js", (c) => {
1847
2426
  return c.text(getBrowserScript(host));
1848
2427
  });
1849
2428
  app.route("/api/logs", logsRoutes(db));
2429
+ app.route("/api/logs/stream", streamRoutes(db));
1850
2430
  app.route("/api/projects", projectsRoutes(db));
1851
2431
  app.route("/api/jobs", jobsRoutes(db));
2432
+ app.route("/api/alerts", alertsRoutes(db));
2433
+ app.route("/api/issues", issuesRoutes(db));
1852
2434
  app.route("/api/perf", perfRoutes(db));
1853
- app.get("/", (c) => c.json({ service: "@hasna/logs", port: PORT, status: "ok" }));
2435
+ app.get("/health", (c) => c.json(getHealth(db)));
2436
+ app.get("/dashboard", (c) => c.redirect("/dashboard/"));
2437
+ app.use("/dashboard/*", serveStatic2({ root: "./dashboard/dist", rewriteRequestPath: (p) => p.replace(/^\/dashboard/, "") }));
2438
+ app.get("/", (c) => c.json({ service: "@hasna/logs", port: PORT, status: "ok", dashboard: `http://localhost:${PORT}/dashboard/` }));
1854
2439
  startScheduler(db);
1855
2440
  console.log(`@hasna/logs server running on http://localhost:${PORT}`);
1856
2441
  var server_default = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/logs",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "description": "Log aggregation + browser script + headless page scanner + performance monitoring for AI agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,6 +12,8 @@
12
12
  },
13
13
  "scripts": {
14
14
  "build": "bun build src/cli/index.ts src/mcp/index.ts src/server/index.ts --outdir dist --target bun --splitting --external playwright --external playwright-core --external electron --external chromium-bidi --external lighthouse",
15
+ "build:dashboard": "cd dashboard && bun run build",
16
+ "build:all": "bun run build:dashboard && bun run build",
15
17
  "dev": "bun run src/server/index.ts",
16
18
  "test": "bun test",
17
19
  "test:coverage": "bun test --coverage",
@@ -21,7 +23,15 @@
21
23
  "access": "restricted",
22
24
  "registry": "https://registry.npmjs.org/"
23
25
  },
24
- "keywords": ["logs", "monitoring", "mcp", "ai-agents", "sentry", "performance", "lighthouse"],
26
+ "keywords": [
27
+ "logs",
28
+ "monitoring",
29
+ "mcp",
30
+ "ai-agents",
31
+ "sentry",
32
+ "performance",
33
+ "lighthouse"
34
+ ],
25
35
  "author": "Andrei Hasna <andrei@hasna.com>",
26
36
  "license": "MIT",
27
37
  "dependencies": {