(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string)
| 1670 | } |
| 1671 | |
| 1672 | private handleTaskScope(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void { |
| 1673 | const taskId = urlPath.split("/")[3]; |
| 1674 | this.readBody(req, async (body) => { |
| 1675 | try { |
| 1676 | const parsed = JSON.parse(body || "{}"); |
| 1677 | const scope = parsed.scope as string; |
| 1678 | if (!["private", "local", "team"].includes(scope)) { |
| 1679 | return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400); |
| 1680 | } |
| 1681 | const task = this.store.getTask(taskId); |
| 1682 | if (!task) return this.jsonResponse(res, { ok: false, error: "task_not_found" }, 404); |
| 1683 | |
| 1684 | if (scope !== "private" && task.status !== "completed") { |
| 1685 | return this.jsonResponse(res, { ok: false, error: "only_completed_tasks_can_be_shared" }, 400); |
| 1686 | } |
| 1687 | |
| 1688 | const isLocalShared = task.owner === "public"; |
| 1689 | const hubTask = this.getHubTaskForLocal(taskId); |
| 1690 | const taskShareUi = this.resolveTaskTeamShareForApi(taskId, hubTask); |
| 1691 | const isTeamShared = taskShareUi.hasHubLink; |
| 1692 | const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private"; |
| 1693 | |
| 1694 | if (scope === currentScope) { |
| 1695 | return this.jsonResponse(res, { ok: true, scope, changed: false }); |
| 1696 | } |
| 1697 | |
| 1698 | let hubSynced = false; |
| 1699 | |
| 1700 | if (scope === "team") { |
| 1701 | if (!isTeamShared) { |
| 1702 | const chunks = this.store.getChunksByTask(taskId); |
| 1703 | const hubClient = await this.resolveHubClientAware(); |
| 1704 | const refreshedTask = this.store.getTask(taskId)!; |
| 1705 | const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", { |
| 1706 | method: "POST", |
| 1707 | body: JSON.stringify({ |
| 1708 | task: { id: refreshedTask.id, sourceTaskId: refreshedTask.id, title: refreshedTask.title, summary: refreshedTask.summary, groupId: null, visibility: "public", createdAt: refreshedTask.startedAt ?? Date.now(), updatedAt: refreshedTask.updatedAt ?? Date.now() }, |
| 1709 | chunks: chunks.map((c) => ({ id: c.id, hubTaskId: refreshedTask.id, sourceTaskId: refreshedTask.id, sourceChunkId: c.id, role: c.role, content: c.content, summary: c.summary, kind: c.kind, groupId: null, visibility: "public", createdAt: c.createdAt ?? Date.now() })), |
| 1710 | }), |
| 1711 | }); |
| 1712 | const hubTaskId = String((response as any)?.taskId ?? ""); |
| 1713 | if (this.sharingRole === "hub" && hubClient.userId) { |
| 1714 | const existing = this.store.getHubTaskBySource(hubClient.userId, taskId); |
| 1715 | this.store.upsertHubTask({ |
| 1716 | id: hubTaskId || existing?.id || crypto.randomUUID(), |
| 1717 | sourceTaskId: taskId, sourceUserId: hubClient.userId, title: refreshedTask.title ?? "", |
| 1718 | summary: refreshedTask.summary ?? "", groupId: null, visibility: "public", |
| 1719 | createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(), |
| 1720 | }); |
| 1721 | } |
| 1722 | const conn = this.store.getClientHubConnection(); |
| 1723 | this.store.markTaskShared(taskId, hubTaskId, chunks.length, "public", null, conn?.hubInstanceId ?? ""); |
| 1724 | hubSynced = true; |
| 1725 | } |
| 1726 | if (!isLocalShared) { |
| 1727 | const originalOwner = task.owner; |
| 1728 | const db = (this.store as any).db; |
| 1729 | db.prepare("INSERT INTO local_shared_tasks (task_id, hub_task_id, original_owner, hub_instance_id, shared_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(task_id) DO UPDATE SET original_owner = excluded.original_owner, hub_instance_id = excluded.hub_instance_id, shared_at = excluded.shared_at").run(taskId, "", originalOwner, "", Date.now()); |
no test coverage detected