@yancyyu/openhermit 1.6.28 → 1.6.29
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/dist-renderer/assets/ProjectEditorOverlay-CQm6jUR1.js +52 -0
- package/dist-renderer/assets/{TeamGraphOverlay-Ba5njic5.js → TeamGraphOverlay-h0WDfifv.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-BvnK-OC1.js → _basePickBy-CgG_tjgX.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-DmFYXx9G.js → _baseUniq-DwPTU9lP.js} +1 -1
- package/dist-renderer/assets/{arc-DX4ZQFY4.js → arc-7nIrGRzY.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DfYr3vEN.js → architectureDiagram-VXUJARFQ-BYhA6Ev2.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-DuXdVeWn.js → blockDiagram-VD42YOAC-BVpZUGDg.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-Bw2nixXe.js → c4Diagram-YG6GDRKO-DsdreMQ9.js} +1 -1
- package/dist-renderer/assets/channel-C0SqeFU7.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-DLiNGQoE.js → chunk-4BX2VUAB-CcoAs7Jd.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-B1L_8VIF.js → chunk-55IACEB6-CGGAOoXd.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-DaZMWKGk.js → chunk-B4BG7PRW-FhpTEPvD.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-ku-dflJG.js → chunk-DI55MBZ5-DoYySbm1.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-DV-mF1dP.js → chunk-FMBD7UC4-e9l2tGHG.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-ByGcDFQ0.js → chunk-QN33PNHL-DeiXVTCy.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-7dv-Min8.js → chunk-QZHKN3VN-DC2UJLJM.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-WdXL5fTu.js → chunk-TZMSLE5B-BHFD9eZI.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +1 -0
- package/dist-renderer/assets/clone-Dm-k63Yr.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-CNcsvqPl.js → cose-bilkent-S5V4N54A-BdybQraU.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-DBNx4qqx.js → dagre-6UL2VRFP-DdF3pwM3.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-BfVlT6sT.js → diagram-PSM6KHXK-B9Ldd3nh.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-HvVjs0K6.js → diagram-QEK2KX5R-XEqkrbpu.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-DYb_KnWS.js → diagram-S2PKOQOG-CipwtY59.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-Ba-IgI5G.js → erDiagram-Q2GNP2WA-BB-2ISGo.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-2iDN8Kpj.js → flowDiagram-NV44I4VS-B8XmJ0u2.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-Byjf8Fa3.js → ganttDiagram-JELNMOA3-D-8XglBb.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DbKvfZ_j.js → gitGraphDiagram-V2S2FVAM-DL4ChakD.js} +1 -1
- package/dist-renderer/assets/{graph-Enirf-f8.js → graph-BiFNoBjP.js} +1 -1
- package/dist-renderer/assets/{index-AjxP_rE_.js → index-6m1ZAymG.js} +1 -1
- package/dist-renderer/assets/index-BhellmRb.css +1 -0
- package/dist-renderer/assets/{index-DY1zqsb6.js → index-BowUl0Jb.js} +540 -536
- package/dist-renderer/assets/{index-CtlzGepK.js → index-Dp3kJTEe.js} +1 -1
- package/dist-renderer/assets/{index-COZPUWJW.js → index-TOpt_T7A.js} +1 -1
- package/dist-renderer/assets/{index-DdhqolqE.js → index-qNBNjW4K.js} +1 -1
- package/dist-renderer/assets/{index-ChR1D6ZF.js → index-vAykq1H1.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D6uicwz1.js → infoDiagram-HS3SLOUP-DRIBfHDi.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DqwZsXlQ.js → journeyDiagram-XKPGCS4Q-BOMiigU4.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-fCDVhVUm.js → kanban-definition-3W4ZIXB7-DDxeyjod.js} +1 -1
- package/dist-renderer/assets/{layout-CPFgj98r.js → layout-DNANbrI4.js} +1 -1
- package/dist-renderer/assets/{linear-CYiQ7Y3M.js → linear-DxEJi1yT.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-D31dS2KE.js → mindmap-definition-VGOIOE7T-nBfGriW8.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BOsCJfds.js → pieDiagram-ADFJNKIX-Din5j6sV.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CYTVQCfr.js → quadrantDiagram-AYHSOK5B-DMVK2BEQ.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CODCFpkt.js → requirementDiagram-UZGBJVZJ-6SC94Gg_.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-Z4ce9ZtZ.js → sankeyDiagram-TZEHDZUN-CD2gghhu.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-CmS9TxhW.js → sequenceDiagram-WL72ISMW-BnhkN7nZ.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-o9k-ns3q.js → stateDiagram-FKZM4ZOC-Bn8XdYX-.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-CxHMyEt1.js → stateDiagram-v2-4FDKWEC3-1b6sI1_g.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-B6T3zrde.js → timeline-definition-IT6M3QCI-CNs3RPoa.js} +1 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +162 -0
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-CleBrdqc.js → xychartDiagram-PRI3JC2R-B8o5J2f3.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/server.ts +699 -179
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +33 -18
- package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +880 -95
- package/src/main/services/teams-mvp/TeamProvisioningService.ts +58 -19
- package/src/main/services/teams-mvp/TeamWorkspaceService.ts +25 -2
- package/src/main/services/teams-mvp/index.ts +3 -0
- package/src/renderer/App.tsx +5 -0
- package/src/renderer/api/httpClient.ts +67 -0
- package/src/renderer/components/layout/PaneContent.tsx +2 -0
- package/src/renderer/components/layout/SortableTab.tsx +1 -0
- package/src/renderer/components/layout/TabBarActions.tsx +12 -12
- package/src/renderer/components/schedules/SchedulesView.tsx +54 -22
- package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +129 -79
- package/src/renderer/components/tasks/TasksView.tsx +343 -0
- package/src/renderer/components/team/TeamDetailView.tsx +20 -98
- package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +1 -1
- package/src/renderer/components/team/editor/EditorContextMenu.tsx +8 -23
- package/src/renderer/components/team/editor/EditorFileTree.tsx +0 -4
- package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +1 -8
- package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +0 -10
- package/src/renderer/components/team/kanban/KanbanBoard.tsx +5 -1
- package/src/renderer/components/team/members/MemberDetailDialog.tsx +8 -33
- package/src/renderer/components/team/messages/MessageComposer.tsx +39 -3
- package/src/renderer/components/team/messages/MessagesPanel.tsx +72 -2
- package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
- package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
- package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
- package/src/renderer/store/slices/scheduleSlice.ts +21 -0
- package/src/renderer/store/slices/teamSlice.ts +59 -23
- package/src/renderer/types/tabs.ts +1 -0
- package/src/shared/types/api.ts +29 -0
- package/src/shared/types/team.ts +104 -1
- package/dist-renderer/assets/ProjectEditorOverlay-A4DZTvSy.js +0 -57
- package/dist-renderer/assets/channel-Pre42N5O.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-CdJsTJsj.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CdJsTJsj.js +0 -1
- package/dist-renderer/assets/clone-BjQBiNfj.js +0 -1
- package/dist-renderer/assets/index-BIOJremZ.css +0 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-CVd5GNDw.js +0 -162
|
@@ -17,6 +17,7 @@ const KEY_WORK_SECONDS = (slug: string) => `hermit:usage:${slug}:workSeconds`;
|
|
|
17
17
|
const KEY_PROJECTS = (slug: string) => `hermit:usage:${slug}:projects`;
|
|
18
18
|
|
|
19
19
|
let scanInterval: ReturnType<typeof setInterval> | null = null;
|
|
20
|
+
let lastLocalScan: TelemetryStatusResult | null = null;
|
|
20
21
|
|
|
21
22
|
function redisConfig(cfg: TaskBusConfig) {
|
|
22
23
|
return {
|
|
@@ -103,12 +104,19 @@ async function uploadMetrics(client: Redis, slug: string, result: ParseResult):
|
|
|
103
104
|
async function doScan(cfg: TaskBusConfig): Promise<ParseResult | null> {
|
|
104
105
|
if (!cfg.telemetry?.enabled) return null;
|
|
105
106
|
|
|
107
|
+
const result = await scanSessions();
|
|
108
|
+
lastLocalScan = statusFromParseResult(result, false);
|
|
109
|
+
|
|
110
|
+
if (!cfg.telemetry.uploadEnabled) {
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
106
114
|
const client = await getRedis(cfg);
|
|
107
|
-
if (!client) return
|
|
115
|
+
if (!client) return result;
|
|
108
116
|
|
|
109
117
|
try {
|
|
110
|
-
const result = await scanSessions();
|
|
111
118
|
await uploadMetrics(client, 'global', result);
|
|
119
|
+
lastLocalScan = statusFromParseResult(result, true);
|
|
112
120
|
return result;
|
|
113
121
|
} finally {
|
|
114
122
|
try {
|
|
@@ -171,15 +179,35 @@ interface TelemetryStatusResult {
|
|
|
171
179
|
workSecondsByDay: Record<string, number>;
|
|
172
180
|
}
|
|
173
181
|
|
|
182
|
+
function statusFromParseResult(result: ParseResult, connected: boolean): TelemetryStatusResult {
|
|
183
|
+
const { aggregate } = result;
|
|
184
|
+
return {
|
|
185
|
+
connected,
|
|
186
|
+
lastScan: new Date().toISOString(),
|
|
187
|
+
sessions: aggregate.sessions,
|
|
188
|
+
messages: aggregate.messages,
|
|
189
|
+
tokensIn: aggregate.tokens.input,
|
|
190
|
+
tokensOut: aggregate.tokens.output,
|
|
191
|
+
cacheRead: aggregate.tokens.cacheRead,
|
|
192
|
+
cacheCreation: aggregate.tokens.cacheCreation,
|
|
193
|
+
activeDays: aggregate.activeDays,
|
|
194
|
+
hourly: aggregate.hourly,
|
|
195
|
+
projects: aggregate.projects,
|
|
196
|
+
workSecondsByDay: aggregate.workSecondsByDay,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
174
200
|
export async function getTelemetryStatus(
|
|
175
|
-
redisCfg
|
|
201
|
+
redisCfg?: TaskBusConfig['redis']
|
|
176
202
|
): Promise<TelemetryStatusResult | null> {
|
|
203
|
+
if (!redisCfg) return lastLocalScan;
|
|
204
|
+
|
|
177
205
|
let Redis: typeof import('ioredis').default;
|
|
178
206
|
try {
|
|
179
207
|
const mod = await import('ioredis');
|
|
180
208
|
Redis = mod.default;
|
|
181
209
|
} catch {
|
|
182
|
-
return
|
|
210
|
+
return lastLocalScan;
|
|
183
211
|
}
|
|
184
212
|
|
|
185
213
|
const cfg = { redis: redisCfg };
|
|
@@ -188,20 +216,7 @@ export async function getTelemetryStatus(
|
|
|
188
216
|
await client.connect();
|
|
189
217
|
await client.ping();
|
|
190
218
|
} catch {
|
|
191
|
-
return
|
|
192
|
-
connected: false,
|
|
193
|
-
lastScan: null,
|
|
194
|
-
sessions: 0,
|
|
195
|
-
messages: 0,
|
|
196
|
-
tokensIn: 0,
|
|
197
|
-
tokensOut: 0,
|
|
198
|
-
cacheRead: 0,
|
|
199
|
-
cacheCreation: 0,
|
|
200
|
-
activeDays: 0,
|
|
201
|
-
hourly: [],
|
|
202
|
-
projects: [],
|
|
203
|
-
workSecondsByDay: {},
|
|
204
|
-
};
|
|
219
|
+
return lastLocalScan;
|
|
205
220
|
}
|
|
206
221
|
|
|
207
222
|
try {
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CollaborationBoardService — canonical state store for cross-team tasks.
|
|
3
|
+
*
|
|
4
|
+
* The collaboration board is a projection of CollabTask state. All meaningful
|
|
5
|
+
* changes must go through transition(), which validates the previous status,
|
|
6
|
+
* bumps version, appends an event, persists locally, and syncs to Redis.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as os from 'os';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
CollabTask,
|
|
15
|
+
CollabTaskEvent,
|
|
16
|
+
CollabTaskEventType,
|
|
17
|
+
CollabTaskStatus,
|
|
18
|
+
} from '@shared/types/team';
|
|
19
|
+
import type Redis from 'ioredis';
|
|
20
|
+
|
|
21
|
+
const HERMIT_HOME = process.env.HERMIT_HOME || path.join(os.homedir(), '.hermit');
|
|
22
|
+
const COLLAB_BOARD_FILE = path.join(HERMIT_HOME, 'collab-board.json');
|
|
23
|
+
const COLLAB_EVENTS_FILE = path.join(HERMIT_HOME, 'collab-events.jsonl');
|
|
24
|
+
|
|
25
|
+
interface TransitionInput {
|
|
26
|
+
dispatchId: string;
|
|
27
|
+
expected: CollabTaskStatus | CollabTaskStatus[];
|
|
28
|
+
next: CollabTaskStatus;
|
|
29
|
+
actor: CollabTaskEvent['actor'];
|
|
30
|
+
eventType: CollabTaskEventType;
|
|
31
|
+
payload?: Record<string, unknown>;
|
|
32
|
+
extra?: Partial<CollabTask>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function nowIso(): string {
|
|
36
|
+
return new Date().toISOString();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeTask(task: CollabTask): CollabTask {
|
|
40
|
+
const now = nowIso();
|
|
41
|
+
return {
|
|
42
|
+
...task,
|
|
43
|
+
version: task.version ?? 1,
|
|
44
|
+
revisionCount: task.revisionCount ?? 0,
|
|
45
|
+
createdAt: task.createdAt || now,
|
|
46
|
+
updatedAt: task.updatedAt || now,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function eventTypeForStatus(status: CollabTaskStatus): CollabTaskEventType {
|
|
51
|
+
switch (status) {
|
|
52
|
+
case 'pending_accept':
|
|
53
|
+
return 'task_sent';
|
|
54
|
+
case 'accepted':
|
|
55
|
+
return 'task_accepted';
|
|
56
|
+
case 'delivered':
|
|
57
|
+
return 'task_delivered';
|
|
58
|
+
case 'approved':
|
|
59
|
+
return 'task_approved';
|
|
60
|
+
case 'revision':
|
|
61
|
+
return 'revision_requested';
|
|
62
|
+
case 'rejected':
|
|
63
|
+
return 'task_rejected';
|
|
64
|
+
case 'failed':
|
|
65
|
+
return 'task_failed';
|
|
66
|
+
default:
|
|
67
|
+
return 'task_failed';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class CollaborationBoardService {
|
|
72
|
+
private tasks: Map<string, CollabTask> = new Map();
|
|
73
|
+
private redis: Redis | null = null;
|
|
74
|
+
private loaded = false;
|
|
75
|
+
|
|
76
|
+
constructor() {
|
|
77
|
+
this.loadFromDisk();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setRedis(redis: Redis | null): void {
|
|
81
|
+
this.redis = redis;
|
|
82
|
+
if (redis) {
|
|
83
|
+
this.syncFromRedis().catch(() => {});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getBoard(): CollabTask[] {
|
|
88
|
+
return Array.from(this.tasks.values()).sort(
|
|
89
|
+
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getTask(dispatchId: string): CollabTask | undefined {
|
|
94
|
+
return this.tasks.get(dispatchId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getEvents(dispatchId: string): CollabTaskEvent[] {
|
|
98
|
+
try {
|
|
99
|
+
if (!fs.existsSync(COLLAB_EVENTS_FILE)) return [];
|
|
100
|
+
return fs
|
|
101
|
+
.readFileSync(COLLAB_EVENTS_FILE, 'utf-8')
|
|
102
|
+
.split(/\r?\n/)
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.map((line) => JSON.parse(line) as CollabTaskEvent)
|
|
105
|
+
.filter((event) => event.dispatchId === dispatchId);
|
|
106
|
+
} catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
addTask(task: CollabTask): CollabTask {
|
|
112
|
+
const existing = this.tasks.get(task.dispatchId);
|
|
113
|
+
if (existing) return existing;
|
|
114
|
+
|
|
115
|
+
const normalized = normalizeTask(task);
|
|
116
|
+
this.tasks.set(normalized.dispatchId, normalized);
|
|
117
|
+
this.appendEvent({
|
|
118
|
+
eventId: crypto.randomUUID(),
|
|
119
|
+
dispatchId: normalized.dispatchId,
|
|
120
|
+
version: normalized.version ?? 1,
|
|
121
|
+
type: 'task_sent',
|
|
122
|
+
actor: { type: 'team', id: normalized.fromTeam },
|
|
123
|
+
payload: {
|
|
124
|
+
fromTeam: normalized.fromTeam,
|
|
125
|
+
toTeam: normalized.toTeam,
|
|
126
|
+
subject: normalized.subject,
|
|
127
|
+
},
|
|
128
|
+
createdAt: nowIso(),
|
|
129
|
+
});
|
|
130
|
+
this.persistToDisk();
|
|
131
|
+
this.syncTaskToRedis(normalized).catch(() => {});
|
|
132
|
+
return normalized;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
transition(input: TransitionInput): CollabTask {
|
|
136
|
+
const task = this.tasks.get(input.dispatchId);
|
|
137
|
+
if (!task) {
|
|
138
|
+
throw new Error(`Collab task not found: ${input.dispatchId}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const expected = Array.isArray(input.expected) ? input.expected : [input.expected];
|
|
142
|
+
if (!expected.includes(task.status)) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Invalid collab task transition: ${task.status} -> ${input.next}; expected ${expected.join(', ')}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const nextVersion = (task.version ?? 1) + 1;
|
|
149
|
+
const nextTask: CollabTask = {
|
|
150
|
+
...task,
|
|
151
|
+
...input.extra,
|
|
152
|
+
status: input.next,
|
|
153
|
+
version: nextVersion,
|
|
154
|
+
updatedAt: nowIso(),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
this.tasks.set(input.dispatchId, nextTask);
|
|
158
|
+
this.appendEvent({
|
|
159
|
+
eventId: crypto.randomUUID(),
|
|
160
|
+
dispatchId: input.dispatchId,
|
|
161
|
+
version: nextVersion,
|
|
162
|
+
type: input.eventType,
|
|
163
|
+
actor: input.actor,
|
|
164
|
+
payload: input.payload,
|
|
165
|
+
createdAt: nowIso(),
|
|
166
|
+
});
|
|
167
|
+
this.persistToDisk();
|
|
168
|
+
this.syncTaskToRedis(nextTask).catch(() => {});
|
|
169
|
+
return nextTask;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Compatibility method for older call sites. New code should prefer transition().
|
|
174
|
+
*/
|
|
175
|
+
updateStatus(
|
|
176
|
+
dispatchId: string,
|
|
177
|
+
status: CollabTaskStatus,
|
|
178
|
+
extra?: Partial<CollabTask>
|
|
179
|
+
): CollabTask | undefined {
|
|
180
|
+
const current = this.tasks.get(dispatchId);
|
|
181
|
+
if (!current) return undefined;
|
|
182
|
+
return this.transition({
|
|
183
|
+
dispatchId,
|
|
184
|
+
expected: current.status,
|
|
185
|
+
next: status,
|
|
186
|
+
actor: { type: 'system', id: 'legacy-updateStatus' },
|
|
187
|
+
eventType: eventTypeForStatus(status),
|
|
188
|
+
payload: extra as Record<string, unknown> | undefined,
|
|
189
|
+
extra,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private loadFromDisk(): void {
|
|
194
|
+
if (this.loaded) return;
|
|
195
|
+
this.loaded = true;
|
|
196
|
+
try {
|
|
197
|
+
if (!fs.existsSync(COLLAB_BOARD_FILE)) return;
|
|
198
|
+
const raw = fs.readFileSync(COLLAB_BOARD_FILE, 'utf-8');
|
|
199
|
+
const arr = JSON.parse(raw) as CollabTask[];
|
|
200
|
+
for (const task of arr) {
|
|
201
|
+
const normalized = normalizeTask(task);
|
|
202
|
+
this.tasks.set(normalized.dispatchId, normalized);
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
// corrupted or missing — start empty
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private persistToDisk(): void {
|
|
210
|
+
try {
|
|
211
|
+
const dir = path.dirname(COLLAB_BOARD_FILE);
|
|
212
|
+
if (!fs.existsSync(dir)) {
|
|
213
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
214
|
+
}
|
|
215
|
+
fs.writeFileSync(COLLAB_BOARD_FILE, JSON.stringify(this.getBoard(), null, 2), 'utf-8');
|
|
216
|
+
} catch {
|
|
217
|
+
// best-effort
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private appendEvent(event: CollabTaskEvent): void {
|
|
222
|
+
try {
|
|
223
|
+
const dir = path.dirname(COLLAB_EVENTS_FILE);
|
|
224
|
+
if (!fs.existsSync(dir)) {
|
|
225
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
226
|
+
}
|
|
227
|
+
fs.appendFileSync(COLLAB_EVENTS_FILE, `${JSON.stringify(event)}\n`, 'utf-8');
|
|
228
|
+
} catch {
|
|
229
|
+
// best-effort
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private async syncTaskToRedis(task: CollabTask): Promise<void> {
|
|
234
|
+
if (!this.redis) return;
|
|
235
|
+
try {
|
|
236
|
+
const score = new Date(task.updatedAt).getTime();
|
|
237
|
+
await this.redis.zadd('collab:board', score, task.dispatchId);
|
|
238
|
+
await this.redis.hset(`collab:task:${task.dispatchId}`, {
|
|
239
|
+
id: task.id,
|
|
240
|
+
dispatchId: task.dispatchId,
|
|
241
|
+
subject: task.subject,
|
|
242
|
+
description: task.description ?? '',
|
|
243
|
+
fromTeam: task.fromTeam,
|
|
244
|
+
fromTeamDisplay: task.fromTeamDisplay,
|
|
245
|
+
toTeam: task.toTeam,
|
|
246
|
+
toTeamDisplay: task.toTeamDisplay,
|
|
247
|
+
status: task.status,
|
|
248
|
+
version: String(task.version ?? 1),
|
|
249
|
+
result: task.result ?? '',
|
|
250
|
+
feedback: task.feedback ?? '',
|
|
251
|
+
deadline: task.deadline ?? '',
|
|
252
|
+
needsHumanReview: String(task.needsHumanReview),
|
|
253
|
+
revisionCount: String(task.revisionCount),
|
|
254
|
+
createdAt: task.createdAt,
|
|
255
|
+
updatedAt: task.updatedAt,
|
|
256
|
+
acceptedAt: task.acceptedAt ?? '',
|
|
257
|
+
deliveredAt: task.deliveredAt ?? '',
|
|
258
|
+
approvedAt: task.approvedAt ?? '',
|
|
259
|
+
});
|
|
260
|
+
} catch {
|
|
261
|
+
// degraded
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async syncToRedis(): Promise<void> {
|
|
266
|
+
if (!this.redis) return;
|
|
267
|
+
for (const task of this.tasks.values()) {
|
|
268
|
+
await this.syncTaskToRedis(task);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async syncFromRedis(): Promise<void> {
|
|
273
|
+
if (!this.redis) return;
|
|
274
|
+
try {
|
|
275
|
+
const ids = await this.redis.zrange('collab:board', 0, -1);
|
|
276
|
+
for (const id of ids) {
|
|
277
|
+
if (this.tasks.has(id)) continue;
|
|
278
|
+
const hash = await this.redis.hgetall(`collab:task:${id}`);
|
|
279
|
+
if (!hash || !hash.dispatchId) continue;
|
|
280
|
+
|
|
281
|
+
const task: CollabTask = normalizeTask({
|
|
282
|
+
id: hash.id,
|
|
283
|
+
dispatchId: hash.dispatchId,
|
|
284
|
+
subject: hash.subject,
|
|
285
|
+
description: hash.description || undefined,
|
|
286
|
+
fromTeam: hash.fromTeam,
|
|
287
|
+
fromTeamDisplay: hash.fromTeamDisplay,
|
|
288
|
+
toTeam: hash.toTeam,
|
|
289
|
+
toTeamDisplay: hash.toTeamDisplay,
|
|
290
|
+
status: hash.status as CollabTaskStatus,
|
|
291
|
+
version: Number(hash.version) || 1,
|
|
292
|
+
result: hash.result || undefined,
|
|
293
|
+
feedback: hash.feedback || undefined,
|
|
294
|
+
deadline: hash.deadline || undefined,
|
|
295
|
+
needsHumanReview: hash.needsHumanReview === 'true',
|
|
296
|
+
revisionCount: Number(hash.revisionCount) || 0,
|
|
297
|
+
createdAt: hash.createdAt,
|
|
298
|
+
updatedAt: hash.updatedAt,
|
|
299
|
+
acceptedAt: hash.acceptedAt || undefined,
|
|
300
|
+
deliveredAt: hash.deliveredAt || undefined,
|
|
301
|
+
approvedAt: hash.approvedAt || undefined,
|
|
302
|
+
});
|
|
303
|
+
this.tasks.set(id, task);
|
|
304
|
+
}
|
|
305
|
+
this.persistToDisk();
|
|
306
|
+
} catch {
|
|
307
|
+
// degraded
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|