@adriandmitroca/relay 0.0.2
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/README.md +121 -0
- package/dist/assets/index-BcE2ldjQ.css +1 -0
- package/dist/assets/index-RaJgQa_m.js +15 -0
- package/dist/index.html +16 -0
- package/package.json +47 -0
- package/scripts/install-service.sh +52 -0
- package/scripts/uninstall-service.sh +10 -0
- package/src/api/config.ts +481 -0
- package/src/api/issues.ts +81 -0
- package/src/api/middleware.ts +14 -0
- package/src/api/router.ts +31 -0
- package/src/cli.ts +184 -0
- package/src/config.ts +195 -0
- package/src/constants.ts +21 -0
- package/src/daemon.ts +1096 -0
- package/src/dashboard.ts +175 -0
- package/src/db.ts +718 -0
- package/src/notifications/telegram.ts +334 -0
- package/src/queue.ts +98 -0
- package/src/sources/asana.ts +161 -0
- package/src/sources/jira.ts +255 -0
- package/src/sources/linear.ts +233 -0
- package/src/sources/sentry.ts +222 -0
- package/src/sources/types.ts +20 -0
- package/src/utils/html.ts +23 -0
- package/src/utils/logger.ts +49 -0
- package/src/worker/claude.ts +297 -0
- package/src/worker/fix.ts +195 -0
- package/src/worker/git.ts +111 -0
- package/src/worker/triage.ts +122 -0
package/src/dashboard.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { IssueDB, IssueRow, ConfigDB } from "./db.ts";
|
|
2
|
+
import type { CallbackAction, CallbackData } from "./constants.ts";
|
|
3
|
+
import { createRouter } from "./api/router.ts";
|
|
4
|
+
import { logger } from "./utils/logger.ts";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
type WsClient = { send(msg: string | Buffer): void };
|
|
8
|
+
|
|
9
|
+
type ActionHandler = (data: CallbackData) => Promise<{ ok: boolean; error?: string; issue?: IssueRow }>;
|
|
10
|
+
type StatusProvider = () => Array<{ workspace: string; telegram: boolean; sources: string[] }>;
|
|
11
|
+
|
|
12
|
+
export class DashboardServer {
|
|
13
|
+
private db: IssueDB;
|
|
14
|
+
private configDB: ConfigDB | null = null;
|
|
15
|
+
private port: number;
|
|
16
|
+
private clients = new Set<WsClient>();
|
|
17
|
+
private server: ReturnType<typeof Bun.serve> | null = null;
|
|
18
|
+
private activeStreams = new Map<number, Record<string, unknown>>();
|
|
19
|
+
private actionHandler: ActionHandler | null = null;
|
|
20
|
+
private statusProvider: StatusProvider | null = null;
|
|
21
|
+
private configChangeHandler: (() => void) | null = null;
|
|
22
|
+
private distDir: string | null = null;
|
|
23
|
+
|
|
24
|
+
constructor(db: IssueDB, port = 7842) {
|
|
25
|
+
this.db = db;
|
|
26
|
+
this.port = port;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setActionHandler(handler: ActionHandler) {
|
|
30
|
+
this.actionHandler = handler;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setStatusProvider(provider: StatusProvider) {
|
|
34
|
+
this.statusProvider = provider;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setConfigDB(configDB: ConfigDB) {
|
|
38
|
+
this.configDB = configDB;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setConfigChangeHandler(handler: () => void) {
|
|
42
|
+
this.configChangeHandler = handler;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setDistDir(dir: string) {
|
|
46
|
+
this.distDir = dir;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
start() {
|
|
50
|
+
const self = this;
|
|
51
|
+
|
|
52
|
+
const app = createRouter({
|
|
53
|
+
getDB: () => this.db,
|
|
54
|
+
getConfigDB: () => {
|
|
55
|
+
if (!this.configDB) throw new Error("ConfigDB not initialized");
|
|
56
|
+
return this.configDB;
|
|
57
|
+
},
|
|
58
|
+
getActionHandler: () => this.actionHandler,
|
|
59
|
+
getStatusProvider: () => this.statusProvider,
|
|
60
|
+
onConfigChange: () => this.configChangeHandler?.(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
this.server = Bun.serve({
|
|
65
|
+
hostname: "127.0.0.1",
|
|
66
|
+
port: this.port,
|
|
67
|
+
|
|
68
|
+
async fetch(req, server) {
|
|
69
|
+
const url = new URL(req.url);
|
|
70
|
+
|
|
71
|
+
// WebSocket upgrade
|
|
72
|
+
if (url.pathname === "/ws") {
|
|
73
|
+
const ok = server.upgrade(req);
|
|
74
|
+
if (!ok) return new Response("WebSocket upgrade failed", { status: 400 });
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// API routes via Hono
|
|
79
|
+
if (url.pathname.startsWith("/api/")) {
|
|
80
|
+
return app.fetch(req);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Serve static files from dist/ if available
|
|
84
|
+
if (self.distDir) {
|
|
85
|
+
// Try exact file match
|
|
86
|
+
let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
87
|
+
const file = Bun.file(join(self.distDir, filePath));
|
|
88
|
+
if (await file.exists()) {
|
|
89
|
+
return new Response(file, {
|
|
90
|
+
headers: { "Content-Type": getMimeType(filePath) },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// SPA fallback: serve index.html for non-file paths
|
|
95
|
+
const indexFile = Bun.file(join(self.distDir, "index.html"));
|
|
96
|
+
if (await indexFile.exists()) {
|
|
97
|
+
return new Response(indexFile, {
|
|
98
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return new Response("Dashboard not built. Run: bun run build", {
|
|
104
|
+
status: 503,
|
|
105
|
+
headers: { "Content-Type": "text/plain" },
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
websocket: {
|
|
110
|
+
open(ws) {
|
|
111
|
+
self.clients.add(ws);
|
|
112
|
+
// Send current stream state to new clients
|
|
113
|
+
for (const event of self.activeStreams.values()) {
|
|
114
|
+
try { ws.send(JSON.stringify(event)); } catch {}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
close(ws) {
|
|
118
|
+
self.clients.delete(ws);
|
|
119
|
+
},
|
|
120
|
+
message() {},
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
logger.info("Dashboard running", { url: `http://localhost:${this.port}` });
|
|
125
|
+
} catch (err) {
|
|
126
|
+
logger.warn("Dashboard failed to start (port may be in use)", { port: this.port, error: String(err) });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
broadcast(event: Record<string, unknown>) {
|
|
131
|
+
if (event.type === "stream_progress") {
|
|
132
|
+
this.activeStreams.set(event.issueId as number, event);
|
|
133
|
+
} else if (event.type === "issue_update") {
|
|
134
|
+
const issue = event.issue as { id: number; status: string };
|
|
135
|
+
if (issue.status === "triaging" || issue.status === "working") {
|
|
136
|
+
// Seed stream with startedAt so clients get a timer immediately
|
|
137
|
+
if (!this.activeStreams.has(issue.id)) {
|
|
138
|
+
this.activeStreams.set(issue.id, {
|
|
139
|
+
type: "stream_progress",
|
|
140
|
+
issueId: issue.id,
|
|
141
|
+
stage: issue.status === "triaging" ? "triage" : "fix",
|
|
142
|
+
startedAt: Date.now(),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
this.activeStreams.delete(issue.id);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!this.clients.size) return;
|
|
151
|
+
const msg = JSON.stringify(event);
|
|
152
|
+
for (const client of this.clients) {
|
|
153
|
+
try {
|
|
154
|
+
client.send(msg);
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
stop() {
|
|
160
|
+
this.server?.stop(true);
|
|
161
|
+
this.server = null;
|
|
162
|
+
this.clients.clear();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getMimeType(path: string): string {
|
|
167
|
+
if (path.endsWith(".html")) return "text/html; charset=utf-8";
|
|
168
|
+
if (path.endsWith(".js")) return "application/javascript";
|
|
169
|
+
if (path.endsWith(".css")) return "text/css";
|
|
170
|
+
if (path.endsWith(".json")) return "application/json";
|
|
171
|
+
if (path.endsWith(".svg")) return "image/svg+xml";
|
|
172
|
+
if (path.endsWith(".png")) return "image/png";
|
|
173
|
+
if (path.endsWith(".ico")) return "image/x-icon";
|
|
174
|
+
return "application/octet-stream";
|
|
175
|
+
}
|