ework-web 0.10.20 → 0.10.21

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.20",
3
+ "version": "0.10.21",
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",
package/src/index.ts CHANGED
@@ -68,6 +68,7 @@ import {
68
68
  archiveLabel,
69
69
  deleteLabel,
70
70
  setIssueLabels,
71
+ listAllProjectIds,
71
72
  type ProjectRole,
72
73
  type UserRow,
73
74
  } from "./store";
@@ -127,6 +128,8 @@ async function refreshOpencodeClient(): Promise<void> {
127
128
  await refreshOpencodeClient();
128
129
  setInterval(refreshOpencodeClient, 10_000);
129
130
 
131
+ void autoWireAllProjects(`http://${cfg.host}:${cfg.port}`);
132
+
130
133
  async function autoWireDaemon(projectId: number, origin: string): Promise<void> {
131
134
  if (!cfg.autowireActive) {
132
135
  log.info("autoWireDaemon: skipped (WORK_AUTOWIRE_ACTIVE=false)", { projectId });
@@ -160,6 +163,24 @@ async function autoWireDaemon(projectId: number, origin: string): Promise<void>
160
163
  }
161
164
  }
162
165
 
166
+ let backfillStarted = false;
167
+ async function autoWireAllProjects(origin: string): Promise<void> {
168
+ if (backfillStarted) return;
169
+ backfillStarted = true;
170
+ if (!cfg.autowireActive || (!cfg.daemonBotLogin.trim() && !cfg.daemonWebhookUrl.trim())) return;
171
+ try {
172
+ const ids = await listAllProjectIds();
173
+ let wired = 0;
174
+ for (const id of ids) {
175
+ await autoWireDaemon(id, origin);
176
+ wired++;
177
+ }
178
+ if (wired > 0) log.info("autoWireAllProjects: backfilled", { count: wired });
179
+ } catch (e) {
180
+ log.warn("autoWireAllProjects failed", { err: e as Error });
181
+ }
182
+ }
183
+
163
184
  const SEC_HEADERS: Record<string, string> = {
164
185
  "content-security-policy": `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'`,
165
186
  "x-content-type-options": "nosniff",
@@ -561,7 +582,7 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
561
582
  const form = await req.formData().catch(() => new FormData());
562
583
  const f: Record<string, string | undefined> = {};
563
584
  for (const [k, v] of form.entries()) f[k] = typeof v === "string" ? v : undefined;
564
- const r = await handleCreateProject(f);
585
+ const r = await handleCreateProject(f, cfg.defaultModel);
565
586
  if (r.projectId) {
566
587
  await ensureProjectBootstrapAdmin(r.projectId, ctx.user!.login);
567
588
  await autoWireDaemon(r.projectId, url.origin);
@@ -973,6 +994,18 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
973
994
  return json({ ok: true });
974
995
  }
975
996
 
997
+ if (req.method === "POST" && url.pathname === "/api/daemons/wire-all") {
998
+ if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
999
+ if (!rateLimit(`daemons-wire:${ip}`, 5, 5 / 3600)) return json({ error: "rate limited" }, 429);
1000
+ try {
1001
+ const ids = await listAllProjectIds();
1002
+ for (const id of ids) await autoWireDaemon(id, url.origin);
1003
+ return json({ ok: true, count: ids.length });
1004
+ } catch (e) {
1005
+ return json({ ok: false, error: errMsg(e) });
1006
+ }
1007
+ }
1008
+
976
1009
  if (req.method === "POST" && url.pathname === "/api/daemons/deploy") {
977
1010
  if (!ctx.user || ctx.user.is_admin !== 1) return json({ error: "admin required" }, 403);
978
1011
  if (!rateLimit(`daemons-deploy:${ip}`, 10, 10 / 3600)) return json({ error: "rate limited" }, 429);
@@ -1931,10 +1964,7 @@ function parseState(s: string | null): "open" | "closed" | "all" {
1931
1964
  async function createProjectSafe(owner: string, name: string) {
1932
1965
  let project = await getProject(owner, name);
1933
1966
  if (project) return project;
1934
- // Auto-create project on first issue POST. Owner/name were already validated by the
1935
- // URL regex shape; allow creation here so `/<owner>/<new-repo>/issues` (POST) bootstraps
1936
- // a project in one step. Use createIssue's tx for atomicity.
1937
- project = await createProject(owner, name, "");
1967
+ project = await createProject(owner, name, "", cfg.defaultModel);
1938
1968
  return project;
1939
1969
  }
1940
1970
 
package/src/store.ts CHANGED
@@ -171,7 +171,12 @@ export async function listProjectsWithCounts(): Promise<ProjectWithCounts[]> {
171
171
  );
172
172
  }
173
173
 
174
- export async function createProject(owner: string, name: string, description: string): Promise<ProjectRow> {
174
+ export async function listAllProjectIds(): Promise<number[]> {
175
+ const rows = await getDB().all<{ id: number }>("SELECT id FROM {{projects}} ORDER BY id");
176
+ return rows.map((r) => r.id);
177
+ }
178
+
179
+ export async function createProject(owner: string, name: string, description: string, model?: string): Promise<ProjectRow> {
175
180
  owner = owner.trim();
176
181
  name = name.trim();
177
182
  if (!/^[A-Za-z0-9_.-]+$/.test(owner)) throw new StoreError(400, "owner 含非法字符");
@@ -180,8 +185,8 @@ export async function createProject(owner: string, name: string, description: st
180
185
  const ts = now();
181
186
  const db = getDB();
182
187
  const info = await db.run(
183
- "INSERT INTO {{projects}} (owner, name, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
184
- [owner, name, description ?? "", ts, ts]
188
+ "INSERT INTO {{projects}} (owner, name, description, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
189
+ [owner, name, description ?? "", model ?? "", ts, ts]
185
190
  );
186
191
  return (await getProjectById(info.insertId))!;
187
192
  }
package/src/views/home.ts CHANGED
@@ -92,14 +92,15 @@ ${tabNavHTML("projects")}
92
92
  }
93
93
 
94
94
  export async function handleCreateProject(
95
- form: Record<string, string | undefined>
95
+ form: Record<string, string | undefined>,
96
+ defaultModel: string,
96
97
  ): Promise<{ location: string; error?: string; projectId?: number }> {
97
98
  const owner = (form.owner ?? "").trim();
98
99
  const name = (form.name ?? "").trim();
99
100
  const description = (form.description ?? "").trim();
100
101
  if (!owner || !name) return { location: "/projects", error: "owner 和 name 必填" };
101
102
  try {
102
- const p = await createProject(owner, name, description);
103
+ const p = await createProject(owner, name, description, defaultModel);
103
104
  return { location: `/${encodeURIComponent(p.owner)}/${encodeURIComponent(p.name)}/issues`, projectId: p.id };
104
105
  } catch (e) {
105
106
  return {