ework-web 0.10.100 → 0.10.101

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-web",
3
- "version": "0.10.100",
3
+ "version": "0.10.101",
4
4
  "type": "module",
5
5
  "description": "ework-web — standalone multi-project issue tracker. Local SQLite-backed, no external API dependency. Bun + TypeScript + SSR HTML.",
6
6
  "license": "MIT",
@@ -1,5 +1,6 @@
1
1
  import { getDB } from "./db";
2
2
  import { log } from "./logger";
3
+ import { loadConfig } from "./config";
3
4
 
4
5
  export interface DaemonInfo {
5
6
  id: number;
@@ -19,6 +20,50 @@ export interface DaemonDetail {
19
20
 
20
21
  const HEARTBEAT_STALE_MS = 120_000;
21
22
 
23
+ // Split-UID deployments keep the daemon registry in the daemon's own SQLite
24
+ // DB, unreachable (and table-absent) from the web process — the \{\{d_*\}\}
25
+ // queries then fail and every lookup silently degrades to "no daemons".
26
+ // Fall back to the router's registry API, which holds the same rows and is
27
+ // the natural source of truth when web and daemon share no database.
28
+ interface RouterDaemonRow {
29
+ id: number;
30
+ displayName: string;
31
+ endpoint: string;
32
+ capacity: number;
33
+ lastHeartbeat: string;
34
+ status: string;
35
+ }
36
+
37
+ let routerCache: { at: number; rows: RouterDaemonRow[] } | null = null;
38
+
39
+ async function routerDaemons(): Promise<RouterDaemonRow[]> {
40
+ if (routerCache && Date.now() - routerCache.at < 15_000) return routerCache.rows;
41
+ try {
42
+ const cfg = await loadConfig();
43
+ if (!cfg.daemonWebhookUrl) return [];
44
+ const res = await fetch(`${cfg.daemonWebhookUrl.replace(/\/$/, "")}/api/daemons`, {
45
+ signal: AbortSignal.timeout(5000),
46
+ });
47
+ const data = (await res.json()) as { daemons?: Array<Partial<RouterDaemonRow>> };
48
+ const rows: RouterDaemonRow[] = (data.daemons ?? [])
49
+ .filter((d): d is Partial<RouterDaemonRow> & { id: number; endpoint: string } =>
50
+ typeof d.id === "number" && typeof d.endpoint === "string")
51
+ .map((d) => ({
52
+ id: d.id,
53
+ displayName: typeof d.displayName === "string" ? d.displayName : `daemon-${d.id}`,
54
+ endpoint: d.endpoint,
55
+ capacity: typeof d.capacity === "number" ? d.capacity : 0,
56
+ lastHeartbeat: typeof d.lastHeartbeat === "string" ? d.lastHeartbeat : "",
57
+ status: typeof d.status === "string" ? d.status : "unknown",
58
+ }));
59
+ routerCache = { at: Date.now(), rows };
60
+ return rows;
61
+ } catch (e) {
62
+ log.info(`coordination: router registry fallback failed (${e instanceof Error ? e.message : String(e)})`);
63
+ return [];
64
+ }
65
+ }
66
+
22
67
  export async function getActiveDaemons(): Promise<DaemonInfo[]> {
23
68
  const stale = new Date(Date.now() - HEARTBEAT_STALE_MS);
24
69
  const staleStr = stale.toISOString().slice(0, 19).replace("T", " ");
@@ -130,19 +175,20 @@ export async function getSessionDaemonMap(): Promise<Map<string, SessionDaemonIn
130
175
 
131
176
  export async function resolveDaemonEndpoint(daemonId: number): Promise<string | null> {
132
177
  try {
133
- const rows = await getDB().all<{ internal_endpoint: string | null }>(
134
- `SELECT internal_endpoint FROM {{d_daemons}} WHERE id = ?`,
178
+ const local = await getDB().all<{ internal_endpoint: string }>(
179
+ "SELECT internal_endpoint FROM {{d_daemons}} WHERE id = ? LIMIT 1",
135
180
  [daemonId],
136
181
  );
137
- const ep = rows[0]?.internal_endpoint;
138
- if (!ep) return null;
139
- return ep;
182
+ const ep = local[0]?.internal_endpoint;
183
+ if (ep) return ep;
140
184
  } catch (e) {
141
- log.info(`coordination: resolve daemon ${daemonId} failed (${e instanceof Error ? e.message : String(e)})`);
142
- return null;
185
+ log.info(`coordination: local daemon lookup failed (${e instanceof Error ? e.message : String(e)})`);
143
186
  }
187
+ const row = (await routerDaemons()).find((d) => d.id === daemonId);
188
+ return row ? row.endpoint : null;
144
189
  }
145
190
 
191
+
146
192
  export interface RunningSessionInfo {
147
193
  issueNumber: string;
148
194
  sessionId: string;