@taskless-app/swarm 0.1.0
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/LICENSE +27 -0
- package/README.md +89 -0
- package/dist/cli.js +2932 -0
- package/dist/overmind.js +4614 -0
- package/docs/HERO.md +98 -0
- package/package.json +61 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2932 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { basename as basename2 } from "path";
|
|
5
|
+
import { createInterface } from "readline/promises";
|
|
6
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
7
|
+
|
|
8
|
+
// src/db.ts
|
|
9
|
+
import Database from "better-sqlite3";
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
import { resolve as resolve2 } from "path";
|
|
12
|
+
|
|
13
|
+
// src/config.ts
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { join, resolve } from "path";
|
|
16
|
+
import { mkdirSync, readFileSync, realpathSync } from "fs";
|
|
17
|
+
var SWARM_HOME = process.env.SWARM_HOME || join(homedir(), ".taskless-swarm");
|
|
18
|
+
mkdirSync(SWARM_HOME, { recursive: true });
|
|
19
|
+
function normalizeRepo(repo) {
|
|
20
|
+
const r = (repo ?? "").trim();
|
|
21
|
+
if (!r) return "";
|
|
22
|
+
try {
|
|
23
|
+
return realpathSync(r);
|
|
24
|
+
} catch {
|
|
25
|
+
return resolve(r);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
var DB_PATH = process.env.SWARM_DB || join(SWARM_HOME, "swarm.db");
|
|
29
|
+
var WORKTREES_DIR = process.env.SWARM_WORKTREES || join(SWARM_HOME, "worktrees");
|
|
30
|
+
var SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : "sqlite";
|
|
31
|
+
var SWARM_OUTBOX = process.env.SWARM_OUTBOX || join(SWARM_HOME, "outbox.jsonl");
|
|
32
|
+
function envNum(name, def) {
|
|
33
|
+
const raw = process.env[name];
|
|
34
|
+
if (raw === void 0 || raw === "") return def;
|
|
35
|
+
const n = Number(raw);
|
|
36
|
+
return Number.isFinite(n) && n >= 0 ? n : def;
|
|
37
|
+
}
|
|
38
|
+
var SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
|
|
39
|
+
var SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
|
|
40
|
+
var HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join(SWARM_HOME, "overmind.heartbeat");
|
|
41
|
+
var HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
|
|
42
|
+
var SWARM_REPO = process.env.SWARM_REPO || "";
|
|
43
|
+
var TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
44
|
+
function tokenFromFile() {
|
|
45
|
+
try {
|
|
46
|
+
return readFileSync(join(SWARM_HOME, "token"), "utf8").trim();
|
|
47
|
+
} catch {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
var TASKLESS_TOKEN = process.env.TASKLESS_TOKEN || tokenFromFile();
|
|
52
|
+
var TASKLESS_WORKSPACE = process.env.TASKLESS_WORKSPACE || "";
|
|
53
|
+
var TASKLESS_WORKSPACE_SLUG = process.env.TASKLESS_WORKSPACE_SLUG || "";
|
|
54
|
+
var TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
|
|
55
|
+
var SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
|
|
56
|
+
var WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
|
|
57
|
+
var WEB_REDIRECT_URI = process.env.SWARM_REDIRECT_URI || `http://127.0.0.1:${WEB_PORT}/callback`;
|
|
58
|
+
|
|
59
|
+
// src/names.ts
|
|
60
|
+
var DRONE_POOL = [
|
|
61
|
+
// Латинский бестиарий (15)
|
|
62
|
+
"SCARAB",
|
|
63
|
+
"VESPA",
|
|
64
|
+
"MANTIS",
|
|
65
|
+
"HORNET",
|
|
66
|
+
"CICADA",
|
|
67
|
+
"LOCUST",
|
|
68
|
+
"ATTA",
|
|
69
|
+
"FORMICA",
|
|
70
|
+
"MYRMEX",
|
|
71
|
+
"APIS",
|
|
72
|
+
"TERMES",
|
|
73
|
+
"GRYLLUS",
|
|
74
|
+
"LAMPYRIS",
|
|
75
|
+
"ARACHNE",
|
|
76
|
+
"SPHINX",
|
|
77
|
+
// Мифо-стаи (9)
|
|
78
|
+
"TIAMAT",
|
|
79
|
+
"JORMUNGAND",
|
|
80
|
+
"GRENDEL",
|
|
81
|
+
"TYPHON",
|
|
82
|
+
"FENRIS",
|
|
83
|
+
"GARM",
|
|
84
|
+
"KUKULKAN",
|
|
85
|
+
"LEVIATHAN",
|
|
86
|
+
"SURTUR"
|
|
87
|
+
];
|
|
88
|
+
var NAME_LIVE = /* @__PURE__ */ new Set(["queued", "running", "blocked", "review"]);
|
|
89
|
+
function toRoman(n) {
|
|
90
|
+
if (n < 1 || n > 3999) return String(n);
|
|
91
|
+
const table = [
|
|
92
|
+
[1e3, "M"],
|
|
93
|
+
[900, "CM"],
|
|
94
|
+
[500, "D"],
|
|
95
|
+
[400, "CD"],
|
|
96
|
+
[100, "C"],
|
|
97
|
+
[90, "XC"],
|
|
98
|
+
[50, "L"],
|
|
99
|
+
[40, "XL"],
|
|
100
|
+
[10, "X"],
|
|
101
|
+
[9, "IX"],
|
|
102
|
+
[5, "V"],
|
|
103
|
+
[4, "IV"],
|
|
104
|
+
[1, "I"]
|
|
105
|
+
];
|
|
106
|
+
let out = "";
|
|
107
|
+
for (const [v, sym] of table) while (n >= v) out += sym, n -= v;
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
function genName(base2, g) {
|
|
111
|
+
return g <= 1 ? base2 : `${base2} ${toRoman(g)}`;
|
|
112
|
+
}
|
|
113
|
+
function baseName(name) {
|
|
114
|
+
return name.split(" ")[0];
|
|
115
|
+
}
|
|
116
|
+
function pickName(rows) {
|
|
117
|
+
const live = /* @__PURE__ */ new Set();
|
|
118
|
+
const lastUse = /* @__PURE__ */ new Map();
|
|
119
|
+
for (const r of rows) {
|
|
120
|
+
if (!r.name) continue;
|
|
121
|
+
if (NAME_LIVE.has(r.status)) live.add(r.name);
|
|
122
|
+
const b = baseName(r.name);
|
|
123
|
+
lastUse.set(b, Math.max(lastUse.get(b) ?? 0, r.id));
|
|
124
|
+
}
|
|
125
|
+
const ordered = [...DRONE_POOL].sort(
|
|
126
|
+
(a, b) => (lastUse.get(a) ?? 0) - (lastUse.get(b) ?? 0) || DRONE_POOL.indexOf(a) - DRONE_POOL.indexOf(b)
|
|
127
|
+
);
|
|
128
|
+
for (let g = 1; ; g++) {
|
|
129
|
+
for (const base2 of ordered) {
|
|
130
|
+
const cand = genName(base2, g);
|
|
131
|
+
if (!live.has(cand)) return cand;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/storm.ts
|
|
137
|
+
var numEnv = (name, def) => {
|
|
138
|
+
const v = Number(process.env[name]);
|
|
139
|
+
return Number.isFinite(v) && v > 0 ? v : def;
|
|
140
|
+
};
|
|
141
|
+
var DEFAULT_STORM_CONFIG = {
|
|
142
|
+
deathThreshold: numEnv("SWARM_STORM_DEATHS", 2),
|
|
143
|
+
windowMs: numEnv("SWARM_STORM_WINDOW_MS", 5 * 60 * 1e3),
|
|
144
|
+
spawnFailThreshold: numEnv("SWARM_STORM_SPAWN_FAILS", 2),
|
|
145
|
+
probeIntervalMs: numEnv("SWARM_STORM_PROBE_INTERVAL_MS", 60 * 1e3),
|
|
146
|
+
probeSuccessesToClear: numEnv("SWARM_STORM_PROBE_CLEAR", 2),
|
|
147
|
+
probeTimeoutMs: numEnv("SWARM_STORM_PROBE_TIMEOUT_MS", 20 * 1e3)
|
|
148
|
+
};
|
|
149
|
+
function encodeProbe(rec) {
|
|
150
|
+
return JSON.stringify(rec);
|
|
151
|
+
}
|
|
152
|
+
function decodeProbe(text) {
|
|
153
|
+
if (!text) return { probe: null, ext: null };
|
|
154
|
+
try {
|
|
155
|
+
const o = JSON.parse(text);
|
|
156
|
+
return {
|
|
157
|
+
probe: typeof o.probe === "boolean" ? o.probe : null,
|
|
158
|
+
ext: typeof o.ext === "boolean" ? o.ext : null
|
|
159
|
+
};
|
|
160
|
+
} catch {
|
|
161
|
+
return { probe: null, ext: null };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/db.ts
|
|
166
|
+
var SCHEMA = `
|
|
167
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
168
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
169
|
+
task_slug TEXT NOT NULL,
|
|
170
|
+
name TEXT,
|
|
171
|
+
kind TEXT NOT NULL DEFAULT 'drone',
|
|
172
|
+
hero_run_id INTEGER,
|
|
173
|
+
repo TEXT,
|
|
174
|
+
worktree_path TEXT,
|
|
175
|
+
branch TEXT,
|
|
176
|
+
status TEXT NOT NULL DEFAULT 'queued',
|
|
177
|
+
pid INTEGER,
|
|
178
|
+
area TEXT,
|
|
179
|
+
depends_on TEXT,
|
|
180
|
+
model TEXT,
|
|
181
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
182
|
+
pr_url TEXT,
|
|
183
|
+
superseded_by INTEGER,
|
|
184
|
+
decision TEXT,
|
|
185
|
+
stop_reason TEXT,
|
|
186
|
+
respawns INTEGER NOT NULL DEFAULT 0,
|
|
187
|
+
resolves_run_id INTEGER,
|
|
188
|
+
task_title TEXT,
|
|
189
|
+
task_desc TEXT,
|
|
190
|
+
task_url TEXT,
|
|
191
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
192
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
193
|
+
);
|
|
194
|
+
CREATE TABLE IF NOT EXISTS questions (
|
|
195
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
196
|
+
run_id INTEGER NOT NULL REFERENCES runs(id),
|
|
197
|
+
text TEXT NOT NULL,
|
|
198
|
+
options TEXT,
|
|
199
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
200
|
+
answer TEXT,
|
|
201
|
+
answered_by TEXT,
|
|
202
|
+
escalated INTEGER NOT NULL DEFAULT 0,
|
|
203
|
+
asked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
204
|
+
answered_at TEXT
|
|
205
|
+
);
|
|
206
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
207
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
208
|
+
run_id INTEGER REFERENCES runs(id),
|
|
209
|
+
kind TEXT NOT NULL,
|
|
210
|
+
text TEXT,
|
|
211
|
+
actor TEXT,
|
|
212
|
+
at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
213
|
+
);
|
|
214
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
|
|
215
|
+
CREATE INDEX IF NOT EXISTS idx_runs_area ON runs(area);
|
|
216
|
+
CREATE INDEX IF NOT EXISTS idx_questions_run ON questions(run_id);
|
|
217
|
+
CREATE INDEX IF NOT EXISTS idx_questions_status ON questions(status);
|
|
218
|
+
CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id);
|
|
219
|
+
-- \u043C\u0430\u0440\u043A\u0435\u0440 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u043D\u044B\u0445 \u0438\u043C\u043F\u043E\u0440\u0442\u043E\u0432 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u0438\u0437 \u0434\u0440\u0443\u0433\u0438\u0445 \u0411\u0414 (\u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C importDb)
|
|
220
|
+
CREATE TABLE IF NOT EXISTS imports (
|
|
221
|
+
source TEXT PRIMARY KEY,
|
|
222
|
+
run_count INTEGER NOT NULL DEFAULT 0,
|
|
223
|
+
imported_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
224
|
+
);
|
|
225
|
+
`;
|
|
226
|
+
function rowToQuestion(row) {
|
|
227
|
+
return {
|
|
228
|
+
id: row.id,
|
|
229
|
+
run_id: row.run_id,
|
|
230
|
+
text: row.text,
|
|
231
|
+
options: row.options ? JSON.parse(row.options) : null,
|
|
232
|
+
status: row.status,
|
|
233
|
+
answer: row.answer ?? null,
|
|
234
|
+
answered_by: row.answered_by ?? null,
|
|
235
|
+
escalated: !!row.escalated,
|
|
236
|
+
asked_at: row.asked_at,
|
|
237
|
+
answered_at: row.answered_at ?? null
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function rowToRun(row) {
|
|
241
|
+
return {
|
|
242
|
+
id: row.id,
|
|
243
|
+
task_slug: row.task_slug,
|
|
244
|
+
// старые прогоны без имени (до этой фичи) → стабильный fallback «#id»
|
|
245
|
+
name: row.name ?? `#${row.id}`,
|
|
246
|
+
// старые прогоны без колонки kind (до ALTER) → "drone" (дефолт колонки)
|
|
247
|
+
kind: row.kind ?? "drone",
|
|
248
|
+
hero_run_id: row.hero_run_id ?? null,
|
|
249
|
+
repo: row.repo ?? "",
|
|
250
|
+
worktree_path: row.worktree_path ?? null,
|
|
251
|
+
branch: row.branch ?? null,
|
|
252
|
+
status: row.status,
|
|
253
|
+
pid: row.pid ?? null,
|
|
254
|
+
area: row.area ?? null,
|
|
255
|
+
depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
|
|
256
|
+
model: row.model ?? null,
|
|
257
|
+
cost_usd: row.cost_usd ?? 0,
|
|
258
|
+
pr_url: row.pr_url ?? null,
|
|
259
|
+
superseded_by: row.superseded_by ?? null,
|
|
260
|
+
decision: row.decision ? JSON.parse(row.decision) : null,
|
|
261
|
+
stop_reason: row.stop_reason ?? null,
|
|
262
|
+
respawns: row.respawns ?? 0,
|
|
263
|
+
// старые прогоны без колонки → 0
|
|
264
|
+
resolves_run_id: row.resolves_run_id ?? null,
|
|
265
|
+
task_title: row.task_title ?? null,
|
|
266
|
+
task_desc: row.task_desc ?? null,
|
|
267
|
+
task_url: row.task_url ?? null,
|
|
268
|
+
created_at: row.created_at,
|
|
269
|
+
updated_at: row.updated_at
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
var SwarmDb = class {
|
|
273
|
+
db;
|
|
274
|
+
defaultRepo;
|
|
275
|
+
constructor(path = "swarm.db", defaultRepo = "") {
|
|
276
|
+
this.defaultRepo = normalizeRepo(defaultRepo);
|
|
277
|
+
this.db = new Database(path);
|
|
278
|
+
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
279
|
+
this.db.exec(SCHEMA);
|
|
280
|
+
for (const alter of [
|
|
281
|
+
"ALTER TABLE questions ADD COLUMN options TEXT",
|
|
282
|
+
"ALTER TABLE questions ADD COLUMN escalated INTEGER NOT NULL DEFAULT 0",
|
|
283
|
+
"ALTER TABLE questions ADD COLUMN answered_by TEXT",
|
|
284
|
+
"ALTER TABLE runs ADD COLUMN superseded_by INTEGER",
|
|
285
|
+
"ALTER TABLE runs ADD COLUMN decision TEXT",
|
|
286
|
+
"ALTER TABLE runs ADD COLUMN stop_reason TEXT",
|
|
287
|
+
"ALTER TABLE runs ADD COLUMN repo TEXT",
|
|
288
|
+
"ALTER TABLE runs ADD COLUMN name TEXT",
|
|
289
|
+
"ALTER TABLE runs ADD COLUMN respawns INTEGER NOT NULL DEFAULT 0",
|
|
290
|
+
"ALTER TABLE runs ADD COLUMN resolves_run_id INTEGER",
|
|
291
|
+
"ALTER TABLE runs ADD COLUMN depends_on TEXT",
|
|
292
|
+
"ALTER TABLE runs ADD COLUMN task_title TEXT",
|
|
293
|
+
"ALTER TABLE runs ADD COLUMN task_desc TEXT",
|
|
294
|
+
"ALTER TABLE runs ADD COLUMN task_url TEXT",
|
|
295
|
+
// Иерархия роя (TASWM-78): роль прогона (drone|hero) и ссылка на прогон-героя.
|
|
296
|
+
"ALTER TABLE runs ADD COLUMN kind TEXT NOT NULL DEFAULT 'drone'",
|
|
297
|
+
"ALTER TABLE runs ADD COLUMN hero_run_id INTEGER",
|
|
298
|
+
"ALTER TABLE events ADD COLUMN actor TEXT"
|
|
299
|
+
]) {
|
|
300
|
+
try {
|
|
301
|
+
this.db.exec(alter);
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_repo ON runs(repo)");
|
|
306
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_hero ON runs(hero_run_id)");
|
|
307
|
+
if (this.defaultRepo) {
|
|
308
|
+
this.db.prepare("UPDATE runs SET repo = ? WHERE repo IS NULL").run(this.defaultRepo);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
close() {
|
|
312
|
+
this.db.close();
|
|
313
|
+
}
|
|
314
|
+
// --- runs ---
|
|
315
|
+
createRun(input) {
|
|
316
|
+
const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
|
|
317
|
+
const res = this.db.prepare(
|
|
318
|
+
"INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, decision, resolves_run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
319
|
+
).run(
|
|
320
|
+
input.task_slug,
|
|
321
|
+
name,
|
|
322
|
+
input.kind ?? "drone",
|
|
323
|
+
// роль прогона: дефолт — рабочий дрон
|
|
324
|
+
input.hero_run_id ?? null,
|
|
325
|
+
// отряд героя (задел иерархии, TASWM-78)
|
|
326
|
+
normalizeRepo(input.repo || this.defaultRepo),
|
|
327
|
+
// канон при записи → один area-lock на физрепо
|
|
328
|
+
input.area ?? null,
|
|
329
|
+
input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
|
|
330
|
+
input.model ?? null,
|
|
331
|
+
input.decision ? JSON.stringify(input.decision) : null,
|
|
332
|
+
input.resolves_run_id ?? null
|
|
333
|
+
);
|
|
334
|
+
const id = Number(res.lastInsertRowid);
|
|
335
|
+
const spawnNote = input.resolves_run_id ? `\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u0434\u043B\u044F #${input.resolves_run_id}` : input.decision ? `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0441 \u0440\u0435\u0448\u0435\u043D\u0438\u0435\u043C \xAB${input.decision.chosen}\xBB` : `run for ${input.task_slug}`;
|
|
336
|
+
this.addEvent(id, "spawned", spawnNote);
|
|
337
|
+
return this.getRun(id);
|
|
338
|
+
}
|
|
339
|
+
getRun(id) {
|
|
340
|
+
const row = this.db.prepare("SELECT * FROM runs WHERE id = ?").get(id);
|
|
341
|
+
return row ? rowToRun(row) : null;
|
|
342
|
+
}
|
|
343
|
+
listRuns(status) {
|
|
344
|
+
const rows = status ? this.db.prepare("SELECT * FROM runs WHERE status = ? ORDER BY id DESC").all(status) : this.db.prepare("SELECT * FROM runs ORDER BY id DESC").all();
|
|
345
|
+
return rows.map(rowToRun);
|
|
346
|
+
}
|
|
347
|
+
/** Прогоны отряда Героя-лейтенанта #heroId (его дроны: hero_run_id = heroId), новые сверху (TASWM-81). */
|
|
348
|
+
squadRuns(heroId) {
|
|
349
|
+
const rows = this.db.prepare("SELECT * FROM runs WHERE hero_run_id = ? ORDER BY id DESC").all(heroId);
|
|
350
|
+
return rows.map(rowToRun);
|
|
351
|
+
}
|
|
352
|
+
updateRun(id, patch) {
|
|
353
|
+
const fields = Object.keys(patch);
|
|
354
|
+
if (fields.length === 0) return this.getRun(id);
|
|
355
|
+
const set = fields.map((f) => `${f} = ?`).join(", ");
|
|
356
|
+
const vals = fields.map((f) => patch[f]);
|
|
357
|
+
this.db.prepare(`UPDATE runs SET ${set}, updated_at = datetime('now') WHERE id = ?`).run(...vals, id);
|
|
358
|
+
return this.getRun(id);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Атомарно увеличить счётчик авто-оживлений прогона (после смерти дрона рой
|
|
362
|
+
* сам его respawn'ит/--continue'ит). Возвращает новое значение — по нему
|
|
363
|
+
* death-detect решает, не исчерпан ли лимит (2 оживления на прогон).
|
|
364
|
+
*/
|
|
365
|
+
incRespawn(id) {
|
|
366
|
+
this.db.prepare("UPDATE runs SET respawns = respawns + 1, updated_at = datetime('now') WHERE id = ?").run(id);
|
|
367
|
+
const row = this.db.prepare("SELECT respawns FROM runs WHERE id = ?").get(id);
|
|
368
|
+
return row?.respawns ?? 0;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Остановить прогон оператором: статус "stopped" + причина + событие.
|
|
372
|
+
* Прогон НЕ удаляется — остаётся в БД и в истории навсегда (в отличие от failed
|
|
373
|
+
* с выброшенным worktree мы храним причину и запись). Kill/cleanup — снаружи (cancelRun).
|
|
374
|
+
*/
|
|
375
|
+
cancel(id, reason) {
|
|
376
|
+
const run = this.updateRun(id, { status: "stopped", stop_reason: reason });
|
|
377
|
+
if (run) this.addEvent(id, "stopped", reason);
|
|
378
|
+
return run;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Живой прогон-резолвер конфликта для review-прогона #id (если есть). «Живой» =
|
|
382
|
+
* не в терминальном статусе: пока такой в работе/на ревью — второго резолвера на тот
|
|
383
|
+
* же PR не поднимаем (иначе на каждый тик плодили бы дронов). stalled/failed/done —
|
|
384
|
+
* не живые: резолвер сорвался/добил → можно, если не исчёрпан лимит попыток.
|
|
385
|
+
*/
|
|
386
|
+
activeResolverFor(reviewRunId) {
|
|
387
|
+
const row = this.db.prepare(
|
|
388
|
+
"SELECT * FROM runs WHERE resolves_run_id = ? AND status NOT IN ('done','failed','superseded','stopped','stalled') ORDER BY id DESC LIMIT 1"
|
|
389
|
+
).get(reviewRunId);
|
|
390
|
+
return row ? rowToRun(row) : null;
|
|
391
|
+
}
|
|
392
|
+
/** Сколько всего резолверов уже поднимали на этот review-PR — потолок авто-попыток. */
|
|
393
|
+
resolverCountFor(reviewRunId) {
|
|
394
|
+
const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE resolves_run_id = ?").get(reviewRunId);
|
|
395
|
+
return row.n;
|
|
396
|
+
}
|
|
397
|
+
/** Последний живой (не терминальный) прогон по slug — для swarm cancel. */
|
|
398
|
+
activeRunForSlug(slug) {
|
|
399
|
+
const row = this.db.prepare(
|
|
400
|
+
"SELECT * FROM runs WHERE task_slug = ? AND status NOT IN ('done','failed','superseded','stopped') ORDER BY id DESC LIMIT 1"
|
|
401
|
+
).get(slug);
|
|
402
|
+
return row ? rowToRun(row) : null;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Зоны, занятые сейчас работающими дронами — для конфликт-осознанной раздачи.
|
|
406
|
+
* С repo — только в этом репозитории (одна area в разных репо не конфликтует).
|
|
407
|
+
* Без repo — по всему рою (обратная совместимость).
|
|
408
|
+
*/
|
|
409
|
+
runningAreas(repo) {
|
|
410
|
+
const base2 = "SELECT DISTINCT area FROM runs WHERE status IN ('running','blocked') AND area IS NOT NULL";
|
|
411
|
+
const rows = repo === void 0 ? this.db.prepare(base2).all() : this.db.prepare(`${base2} AND repo = ?`).all(repo);
|
|
412
|
+
return rows.map((r) => r.area);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Из depends_on прогона — слаги зависимостей, ещё НЕ закрытых (нет прогона со
|
|
416
|
+
* статусом done). Пусто → все выполнены, дрона можно спавнить. Явная зависимость
|
|
417
|
+
* (spawn --after) отдельна от area-lock: area освобождается на review (до мержа),
|
|
418
|
+
* а тут ждём именно done (смерж), чтобы следующий дрон стартовал от кода предыдущего.
|
|
419
|
+
* Несуществующий/зациклённый слаг просто никогда не станет done → прогон вечно ждёт,
|
|
420
|
+
* но очередь не виснет (гейт — пропуск в цикле, не блокировка).
|
|
421
|
+
*/
|
|
422
|
+
unmetDeps(depends_on) {
|
|
423
|
+
if (!depends_on || depends_on.length === 0) return [];
|
|
424
|
+
const rows = this.db.prepare("SELECT DISTINCT task_slug FROM runs WHERE status = 'done' AND resolves_run_id IS NULL").all();
|
|
425
|
+
const done = new Set(rows.map((r) => r.task_slug));
|
|
426
|
+
return depends_on.filter((slug) => !done.has(slug));
|
|
427
|
+
}
|
|
428
|
+
/** Все репо, засветившиеся в прогонах — whitelist для валидации spawn из веба. */
|
|
429
|
+
distinctRepos() {
|
|
430
|
+
const rows = this.db.prepare("SELECT DISTINCT repo FROM runs WHERE repo IS NOT NULL AND repo <> ''").all();
|
|
431
|
+
return rows.map((r) => r.repo);
|
|
432
|
+
}
|
|
433
|
+
// --- импорт истории из другой БД (объединение роёв) ---
|
|
434
|
+
/**
|
|
435
|
+
* Идемпотентный перенос runs/events/questions из другой swarm-БД в эту.
|
|
436
|
+
* id прогонов ремапятся (старый→новый); run_id в events/questions и superseded_by
|
|
437
|
+
* в runs правятся по карте; всем импортируемым прогонам проставляется repo (норм.).
|
|
438
|
+
* pid НЕ переносим — чужой pid в объединённой БД бессмыслен и мог бы совпасть с
|
|
439
|
+
* живым процессом (ложный liveness); импортируемые прогоны исторические.
|
|
440
|
+
* Живые статусы нормализуем (running/blocked→stalled, queued→stopped) и открытые
|
|
441
|
+
* вопросы не тащим — иначе импорт даёт вечных зомби: без pid death-detect их
|
|
442
|
+
* пропускает, они навсегда держат (repo,area)-лок и «в работе» в метриках, а их
|
|
443
|
+
* открытые вопросы «воскрешают» архивный прогон через answer.
|
|
444
|
+
* Повторный вызов на том же source — no-op (маркер в таблице imports). Source
|
|
445
|
+
* открываем на ЗАПИСЬ (importDb только ЧИТАЕТ из него): все БД роя — WAL, а bun:sqlite
|
|
446
|
+
* с readonly НЕ открывает WAL-source без -shm (реальный перенос обычно = копия одного
|
|
447
|
+
* файла swarm.db без sidecar'ов → SQLITE_CANTOPEN). Несуществующий путь ловим existsSync
|
|
448
|
+
* ДО открытия — честная ошибка, а не пустой файл (rw создал бы его). Всё в одной
|
|
449
|
+
* транзакции: прервётся — маркер не встанет, повтор безопасен.
|
|
450
|
+
*/
|
|
451
|
+
importDb(sourcePath, repo) {
|
|
452
|
+
const abs = resolve2(sourcePath);
|
|
453
|
+
const repoN = normalizeRepo(repo);
|
|
454
|
+
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
455
|
+
if (already) return { imported: false, runs: already.run_count };
|
|
456
|
+
if (!existsSync(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
|
|
457
|
+
const src = new Database(abs);
|
|
458
|
+
try {
|
|
459
|
+
const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
|
|
460
|
+
const events = src.prepare("SELECT * FROM events ORDER BY id").all();
|
|
461
|
+
const questions = src.prepare("SELECT * FROM questions ORDER BY id").all();
|
|
462
|
+
const txn = this.db.transaction(() => {
|
|
463
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
464
|
+
const insRun = this.db.prepare(
|
|
465
|
+
"INSERT INTO runs (task_slug, repo, worktree_path, branch, status, area, model, cost_usd, pr_url, decision, stop_reason, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
|
466
|
+
);
|
|
467
|
+
for (const r of runs) {
|
|
468
|
+
const raw = r.status ?? "done";
|
|
469
|
+
let status = raw;
|
|
470
|
+
let stopReason = r.stop_reason ?? null;
|
|
471
|
+
let normEvent = null;
|
|
472
|
+
if (raw === "running" || raw === "blocked") {
|
|
473
|
+
status = "stalled";
|
|
474
|
+
normEvent = { kind: "stalled", text: "\u0438\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D \u0438\u0437 \u0430\u0440\u0445\u0438\u0432\u0430: \u0441\u0442\u0430\u0442\u0443\u0441 \u043D\u043E\u0440\u043C\u0430\u043B\u0438\u0437\u043E\u0432\u0430\u043D" };
|
|
475
|
+
} else if (raw === "queued") {
|
|
476
|
+
status = "stopped";
|
|
477
|
+
stopReason = "\u0430\u0440\u0445\u0438\u0432\u043D\u044B\u0439 \u0438\u043C\u043F\u043E\u0440\u0442";
|
|
478
|
+
normEvent = { kind: "stopped", text: "\u0430\u0440\u0445\u0438\u0432\u043D\u044B\u0439 \u0438\u043C\u043F\u043E\u0440\u0442" };
|
|
479
|
+
}
|
|
480
|
+
const res = insRun.run(
|
|
481
|
+
r.task_slug,
|
|
482
|
+
repoN,
|
|
483
|
+
r.worktree_path ?? null,
|
|
484
|
+
r.branch ?? null,
|
|
485
|
+
status,
|
|
486
|
+
r.area ?? null,
|
|
487
|
+
r.model ?? null,
|
|
488
|
+
r.cost_usd ?? 0,
|
|
489
|
+
r.pr_url ?? null,
|
|
490
|
+
r.decision ?? null,
|
|
491
|
+
stopReason,
|
|
492
|
+
r.created_at ?? null,
|
|
493
|
+
r.updated_at ?? null
|
|
494
|
+
);
|
|
495
|
+
const newId = Number(res.lastInsertRowid);
|
|
496
|
+
idMap.set(r.id, newId);
|
|
497
|
+
if (normEvent) this.addEvent(newId, normEvent.kind, normEvent.text);
|
|
498
|
+
}
|
|
499
|
+
const setSup = this.db.prepare("UPDATE runs SET superseded_by = ? WHERE id = ?");
|
|
500
|
+
for (const r of runs) {
|
|
501
|
+
const sup = r.superseded_by;
|
|
502
|
+
if (sup != null && idMap.has(sup)) setSup.run(idMap.get(sup), idMap.get(r.id));
|
|
503
|
+
}
|
|
504
|
+
const insEv = this.db.prepare("INSERT INTO events (run_id, kind, text, actor, at) VALUES (?,?,?,?,?)");
|
|
505
|
+
for (const e of events) {
|
|
506
|
+
const oldRun = e.run_id;
|
|
507
|
+
const newRun = oldRun != null ? idMap.get(oldRun) ?? null : null;
|
|
508
|
+
insEv.run(newRun, e.kind, e.text ?? null, e.actor ?? null, e.at ?? null);
|
|
509
|
+
}
|
|
510
|
+
const insQ = this.db.prepare(
|
|
511
|
+
"INSERT INTO questions (run_id, text, options, status, answer, answered_by, escalated, asked_at, answered_at) VALUES (?,?,?,?,?,?,?,?,?)"
|
|
512
|
+
);
|
|
513
|
+
for (const q of questions) {
|
|
514
|
+
if (q.status === "open") continue;
|
|
515
|
+
const newRun = idMap.get(q.run_id);
|
|
516
|
+
if (newRun == null) continue;
|
|
517
|
+
insQ.run(
|
|
518
|
+
newRun,
|
|
519
|
+
q.text,
|
|
520
|
+
q.options ?? null,
|
|
521
|
+
q.status ?? "answered",
|
|
522
|
+
q.answer ?? null,
|
|
523
|
+
q.answered_by ?? null,
|
|
524
|
+
q.escalated ?? 0,
|
|
525
|
+
q.asked_at ?? null,
|
|
526
|
+
q.answered_at ?? null
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
this.db.prepare("INSERT INTO imports (source, run_count) VALUES (?, ?)").run(abs, runs.length);
|
|
530
|
+
});
|
|
531
|
+
txn();
|
|
532
|
+
return { imported: true, runs: runs.length };
|
|
533
|
+
} finally {
|
|
534
|
+
src.close();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// --- questions (петля ask/answer) ---
|
|
538
|
+
ask(run_id, text, options) {
|
|
539
|
+
const res = this.db.prepare("INSERT INTO questions (run_id, text, options) VALUES (?, ?, ?)").run(run_id, text, options && options.length ? JSON.stringify(options) : null);
|
|
540
|
+
const id = Number(res.lastInsertRowid);
|
|
541
|
+
this.updateRun(run_id, { status: "blocked" });
|
|
542
|
+
this.addEvent(run_id, "blocked", text);
|
|
543
|
+
return this.getQuestion(id);
|
|
544
|
+
}
|
|
545
|
+
/** Оркестратор поднял вопрос человеку (техническое решил бы сам). */
|
|
546
|
+
escalateQuestion(id) {
|
|
547
|
+
this.db.prepare("UPDATE questions SET escalated=1 WHERE id=?").run(id);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Снять открытый вопрос без реального ответа (дрон замер: overmind недоступен,
|
|
551
|
+
* ответа не будет — TASWM-41). Отдельный статус 'withdrawn' (не 'answered'): вопрос
|
|
552
|
+
* уходит из openQuestions() (WHERE status='open'), защищён от answer() (тот правит
|
|
553
|
+
* только open И воскрешает прогон лишь на статусе 'answered') — так снятый вопрос
|
|
554
|
+
* не оживит замерший прогон. Остаётся в истории как снятый с пояснением.
|
|
555
|
+
*/
|
|
556
|
+
withdrawQuestion(id, reason) {
|
|
557
|
+
this.db.prepare("UPDATE questions SET status='withdrawn', answer=?, answered_at=datetime('now') WHERE id=? AND status='open'").run(reason, id);
|
|
558
|
+
}
|
|
559
|
+
answer(question_id, answer, by = "human") {
|
|
560
|
+
this.db.prepare(
|
|
561
|
+
"UPDATE questions SET status='answered', answer=?, answered_by=?, answered_at=datetime('now') WHERE id=? AND status='open'"
|
|
562
|
+
).run(answer, by, question_id);
|
|
563
|
+
const q = this.getQuestion(question_id);
|
|
564
|
+
if (q && q.status === "answered") {
|
|
565
|
+
this.updateRun(q.run_id, { status: "running" });
|
|
566
|
+
this.addEvent(q.run_id, "answered", answer, by);
|
|
567
|
+
}
|
|
568
|
+
return q;
|
|
569
|
+
}
|
|
570
|
+
/** Пометить старый прогон переигранным на новый (откат на другой вариант). */
|
|
571
|
+
supersede(oldId, newId, by = "human") {
|
|
572
|
+
this.updateRun(oldId, { superseded_by: newId });
|
|
573
|
+
this.addEvent(oldId, "superseded", `\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u2192 #${newId}`, by);
|
|
574
|
+
}
|
|
575
|
+
getQuestion(id) {
|
|
576
|
+
const row = this.db.prepare("SELECT * FROM questions WHERE id = ?").get(id);
|
|
577
|
+
return row ? rowToQuestion(row) : null;
|
|
578
|
+
}
|
|
579
|
+
openQuestions() {
|
|
580
|
+
return this.db.prepare("SELECT * FROM questions WHERE status='open' ORDER BY id").all().map(
|
|
581
|
+
rowToQuestion
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
/** id прогонов, у которых есть хотя бы один вопрос — доступность «Переиграть» в модалке. */
|
|
585
|
+
runIdsWithQuestions() {
|
|
586
|
+
return this.db.prepare("SELECT DISTINCT run_id FROM questions").all().map((r) => r.run_id);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Открытые вопросы дронов отряда Героя #heroId (join questions×runs по hero_run_id).
|
|
590
|
+
* Герой видит их в squad_status и отвечает через answer_drone; вопрос не из отряда сюда
|
|
591
|
+
* не попадёт — это и есть гейт «отвечать только своим» (TASWM-81).
|
|
592
|
+
*/
|
|
593
|
+
openQuestionsFor(heroId) {
|
|
594
|
+
return this.db.prepare(
|
|
595
|
+
"SELECT q.* FROM questions q JOIN runs r ON r.id = q.run_id WHERE r.hero_run_id = ? AND q.status = 'open' ORDER BY q.id"
|
|
596
|
+
).all(heroId).map(rowToQuestion);
|
|
597
|
+
}
|
|
598
|
+
// --- events (лог/хроника) ---
|
|
599
|
+
addEvent(run_id, kind, text, actor = null) {
|
|
600
|
+
this.db.prepare("INSERT INTO events (run_id, kind, text, actor) VALUES (?, ?, ?, ?)").run(run_id, kind, text ?? null, actor);
|
|
601
|
+
}
|
|
602
|
+
events(run_id) {
|
|
603
|
+
return this.db.prepare("SELECT * FROM events WHERE run_id = ? ORDER BY id").all(run_id);
|
|
604
|
+
}
|
|
605
|
+
/** Хроника роя (вкладка История): Q&A (вопрос+варианты+ответ+автор) + вехи прогонов, новые сверху. */
|
|
606
|
+
history(limit = 50) {
|
|
607
|
+
const items = [];
|
|
608
|
+
const name = (row) => row.name ?? `#${row.run_id}`;
|
|
609
|
+
for (const row of this.db.prepare(
|
|
610
|
+
"SELECT q.*, r.task_slug slug, r.area area, r.name name, r.repo repo, r.task_title title, r.pr_url pr, r.task_url taskUrl FROM questions q LEFT JOIN runs r ON r.id = q.run_id"
|
|
611
|
+
).all()) {
|
|
612
|
+
const q = rowToQuestion(row);
|
|
613
|
+
items.push({
|
|
614
|
+
kind: "qa",
|
|
615
|
+
at: row.answered_at ?? q.asked_at,
|
|
616
|
+
slug: row.slug ?? "?",
|
|
617
|
+
name: name(row),
|
|
618
|
+
repo: row.repo ?? "",
|
|
619
|
+
area: row.area ?? null,
|
|
620
|
+
qid: q.id,
|
|
621
|
+
question: q.text,
|
|
622
|
+
options: q.options,
|
|
623
|
+
answer: q.answer,
|
|
624
|
+
answered_by: q.answered_by,
|
|
625
|
+
escalated: q.escalated,
|
|
626
|
+
open: q.status === "open",
|
|
627
|
+
title: row.title ?? null,
|
|
628
|
+
pr: row.pr ?? null,
|
|
629
|
+
taskUrl: row.taskUrl ?? null
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
const MILESTONES = /* @__PURE__ */ new Set(["spawned", "pr_opened", "merged", "done", "failed", "stalled", "conflict", "stopped", "superseded", "paused", "resumed", "storm_start", "storm_end", "storm_dead"]);
|
|
633
|
+
for (const row of this.db.prepare(
|
|
634
|
+
"SELECT e.*, r.task_slug slug, r.name name, r.repo repo, r.task_title title, r.pr_url pr, r.task_url taskUrl FROM events e LEFT JOIN runs r ON r.id = e.run_id"
|
|
635
|
+
).all()) {
|
|
636
|
+
if (!MILESTONES.has(row.kind)) continue;
|
|
637
|
+
const global = row.run_id == null;
|
|
638
|
+
items.push({
|
|
639
|
+
kind: "milestone",
|
|
640
|
+
at: row.at,
|
|
641
|
+
slug: row.slug ?? (global ? "\u0440\u043E\u0439" : "?"),
|
|
642
|
+
name: global ? "\u0440\u043E\u0439" : name(row),
|
|
643
|
+
repo: row.repo ?? "",
|
|
644
|
+
event: row.kind,
|
|
645
|
+
text: row.text ?? null,
|
|
646
|
+
actor: row.actor ?? null,
|
|
647
|
+
title: row.title ?? null,
|
|
648
|
+
pr: row.pr ?? null,
|
|
649
|
+
taskUrl: row.taskUrl ?? null
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
items.sort((a, b) => a.at < b.at ? 1 : a.at > b.at ? -1 : 0);
|
|
653
|
+
return items.slice(0, limit);
|
|
654
|
+
}
|
|
655
|
+
/** Сколько PR смержено сегодня (UTC) — для метрики «Смержено сегодня» на Главной. */
|
|
656
|
+
mergedToday() {
|
|
657
|
+
const row = this.db.prepare("SELECT COUNT(*) n FROM events WHERE kind = 'merged' AND date(at) = date('now')").get();
|
|
658
|
+
return row.n;
|
|
659
|
+
}
|
|
660
|
+
// --- штормовой режим (TASWM-23): состояние роя в событиях (source of truth) ---
|
|
661
|
+
/**
|
|
662
|
+
* В шторме ли рой сейчас: смотрим последнее ГЛОБАЛЬНОЕ (run_id NULL) событие
|
|
663
|
+
* storm_start/storm_end. storm_start без последующего storm_end → шторм активен.
|
|
664
|
+
* Выводится из событий → переживает рестарт overmind.
|
|
665
|
+
*/
|
|
666
|
+
stormActive() {
|
|
667
|
+
const row = this.db.prepare("SELECT kind FROM events WHERE run_id IS NULL AND kind IN ('storm_start','storm_end') ORDER BY id DESC LIMIT 1").get();
|
|
668
|
+
return row?.kind === "storm_start";
|
|
669
|
+
}
|
|
670
|
+
/** Время начала ТЕКУЩЕГО шторма (at события storm_start) или null, если шторма нет. */
|
|
671
|
+
currentStormStartedAt() {
|
|
672
|
+
const row = this.db.prepare("SELECT kind, at FROM events WHERE run_id IS NULL AND kind IN ('storm_start','storm_end') ORDER BY id DESC LIMIT 1").get();
|
|
673
|
+
return row?.kind === "storm_start" ? row.at : null;
|
|
674
|
+
}
|
|
675
|
+
/** Пометить вход в шторм (глобальное событие). Идемпотентность — на стороне вызывающего. */
|
|
676
|
+
startStorm(reason) {
|
|
677
|
+
this.addEvent(null, "storm_start", reason);
|
|
678
|
+
}
|
|
679
|
+
/** Пометить отбой шторма (глобальное событие). */
|
|
680
|
+
endStorm(reason) {
|
|
681
|
+
this.addEvent(null, "storm_end", reason);
|
|
682
|
+
}
|
|
683
|
+
/** Записать смерть работающего дрона — семпл для скользящего окна детекции шторма. */
|
|
684
|
+
recordDeath(runId, note) {
|
|
685
|
+
this.addEvent(runId, "died", note ?? "\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440 (dead pid)");
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Смертей дронов (событий died) с момента cutoff. cutoffSeconds — epoch-СЕКУНДЫ;
|
|
689
|
+
* datetime(?, 'unixepoch') форматирует его тем же «YYYY-MM-DD HH:MM:SS» UTC, что и
|
|
690
|
+
* datetime('now') в at — строковое сравнение корректно, без парса на стороне JS.
|
|
691
|
+
*/
|
|
692
|
+
deathCountSince(cutoffSeconds) {
|
|
693
|
+
const row = this.db.prepare("SELECT COUNT(*) n FROM events WHERE kind='died' AND at >= datetime(?, 'unixepoch')").get(cutoffSeconds);
|
|
694
|
+
return row.n;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Спавн-фейлов (событий failed на НЕ-резолверных прогонах) с момента cutoff.
|
|
698
|
+
* Резолверные failed (merge-main-in) не считаем штормовым сигналом.
|
|
699
|
+
*/
|
|
700
|
+
spawnFailCountSince(cutoffSeconds) {
|
|
701
|
+
const row = this.db.prepare(
|
|
702
|
+
"SELECT COUNT(*) n FROM events e JOIN runs r ON r.id = e.run_id WHERE e.kind='failed' AND r.resolves_run_id IS NULL AND e.at >= datetime(?, 'unixepoch')"
|
|
703
|
+
).get(cutoffSeconds);
|
|
704
|
+
return row.n;
|
|
705
|
+
}
|
|
706
|
+
/** Записать результат периодической проверки шторма (health-probe + внешний сигнал). */
|
|
707
|
+
recordStormProbe(probe, ext) {
|
|
708
|
+
this.addEvent(null, "storm_probe", encodeProbe({ probe, ext }));
|
|
709
|
+
}
|
|
710
|
+
/** Последняя запись storm_probe (для троттлинга по времени и последнего внешнего сигнала). */
|
|
711
|
+
latestStormProbe() {
|
|
712
|
+
const row = this.db.prepare("SELECT text, at FROM events WHERE run_id IS NULL AND kind='storm_probe' ORDER BY id DESC LIMIT 1").get();
|
|
713
|
+
return row ? { rec: decodeProbe(row.text), at: row.at } : null;
|
|
714
|
+
}
|
|
715
|
+
/** Записи storm_probe текущего шторма (at >= sinceIso), по возрастанию — для счётчика успехов подряд. */
|
|
716
|
+
stormProbeRecordsSince(sinceIso) {
|
|
717
|
+
const rows = this.db.prepare("SELECT text FROM events WHERE run_id IS NULL AND kind='storm_probe' AND at >= ? ORDER BY id").all(sinceIso);
|
|
718
|
+
return rows.map((r) => decodeProbe(r.text));
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
// src/web.ts
|
|
723
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
724
|
+
import { basename } from "path";
|
|
725
|
+
import { serve } from "@hono/node-server";
|
|
726
|
+
import { $ as $2 } from "execa";
|
|
727
|
+
|
|
728
|
+
// ../swarm-core/src/timeline.ts
|
|
729
|
+
function buildSegments(created, events, laneEnd) {
|
|
730
|
+
const segs = [];
|
|
731
|
+
let cursor = created;
|
|
732
|
+
let phase = "queued";
|
|
733
|
+
const push = (to) => {
|
|
734
|
+
if (to > cursor) segs.push({ phase, from: cursor, to });
|
|
735
|
+
cursor = to;
|
|
736
|
+
};
|
|
737
|
+
for (const e of events) {
|
|
738
|
+
switch (e.kind) {
|
|
739
|
+
case "step":
|
|
740
|
+
if (phase === "queued") {
|
|
741
|
+
push(e.at);
|
|
742
|
+
phase = "running";
|
|
743
|
+
}
|
|
744
|
+
break;
|
|
745
|
+
case "blocked":
|
|
746
|
+
push(e.at);
|
|
747
|
+
phase = "blocked";
|
|
748
|
+
break;
|
|
749
|
+
case "answered":
|
|
750
|
+
push(e.at);
|
|
751
|
+
phase = "running";
|
|
752
|
+
break;
|
|
753
|
+
case "pr_opened":
|
|
754
|
+
push(e.at);
|
|
755
|
+
phase = "review";
|
|
756
|
+
break;
|
|
757
|
+
case "merged":
|
|
758
|
+
case "done":
|
|
759
|
+
push(e.at);
|
|
760
|
+
phase = "done";
|
|
761
|
+
break;
|
|
762
|
+
case "failed":
|
|
763
|
+
push(e.at);
|
|
764
|
+
phase = "failed";
|
|
765
|
+
break;
|
|
766
|
+
case "stalled":
|
|
767
|
+
push(e.at);
|
|
768
|
+
phase = "stalled";
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
push(laneEnd);
|
|
773
|
+
return segs;
|
|
774
|
+
}
|
|
775
|
+
function computeBlocker(status, stopReason, lastStep, pr, ci, deps, openQ) {
|
|
776
|
+
switch (status) {
|
|
777
|
+
case "queued": {
|
|
778
|
+
const unmet = deps.filter((d) => !d.met);
|
|
779
|
+
if (unmet.length) {
|
|
780
|
+
const who = unmet.map((d) => d.name ? `${d.slug}\xB7${d.name}` : d.slug).join(", ");
|
|
781
|
+
return { icon: "\u26D4", text: `\u0436\u0434\u0451\u0442 ${who}`, tone: "dep" };
|
|
782
|
+
}
|
|
783
|
+
return { icon: "\u23F8", text: "\u0436\u0434\u0451\u0442 \u0441\u043B\u043E\u0442\u0430 / \u0441\u043F\u0430\u0432\u043D\u0430", tone: "queued" };
|
|
784
|
+
}
|
|
785
|
+
case "running":
|
|
786
|
+
return { icon: "\u25B6", text: lastStep ? `\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442: ${lastStep}` : "\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", tone: "run" };
|
|
787
|
+
case "blocked":
|
|
788
|
+
return { icon: "\u2753", text: openQ ? `\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430: ${openQ}` : "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430", tone: "blocked" };
|
|
789
|
+
case "paused":
|
|
790
|
+
return { icon: "\u{1F4A4}", text: "\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430", tone: "blocked" };
|
|
791
|
+
case "review": {
|
|
792
|
+
if (!pr) return { icon: "\u{1F440}", text: "review \u0431\u0435\u0437 PR", tone: "review" };
|
|
793
|
+
if (!ci) return { icon: "\u23F3", text: "CI: \u0436\u0434\u0443 \u0441\u0442\u0430\u0442\u0443\u0441\u2026", tone: "review" };
|
|
794
|
+
if (ci.tone === "merge") return { icon: "\u{1F535}", text: "CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u043C\u0435\u0440\u0436\u0430", tone: "merge" };
|
|
795
|
+
return { icon: ci.tone === "fail" ? "\u274C" : "\u23F3", text: ci.text.replace(/^[^ ]+ /, ""), tone: ci.tone };
|
|
796
|
+
}
|
|
797
|
+
case "stalled":
|
|
798
|
+
return { icon: "\u26A0\uFE0F", text: stopReason || (lastStep ? `\u043C\u043E\u043B\u0447\u0438\u0442: ${lastStep}` : "\u043C\u043E\u043B\u0447\u0438\u0442"), tone: "stalled" };
|
|
799
|
+
case "failed":
|
|
800
|
+
return { icon: "\u274C", text: stopReason || "\u0443\u043F\u0430\u043B", tone: "fail" };
|
|
801
|
+
case "stopped":
|
|
802
|
+
return { icon: "\u23F9", text: stopReason || "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", tone: "queued" };
|
|
803
|
+
case "done":
|
|
804
|
+
return { icon: "\u2705", text: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", tone: "done" };
|
|
805
|
+
default:
|
|
806
|
+
return { icon: "\u2022", text: status, tone: "queued" };
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function pickHeadline(lanes) {
|
|
810
|
+
const live = lanes.filter((l2) => l2.live && !l2.obsolete);
|
|
811
|
+
const by = (pred) => live.find(pred);
|
|
812
|
+
const fmt = (l2, extra) => `${l2.name} \xB7 ${l2.slug} \u2014 ${extra}`;
|
|
813
|
+
let l = by((x) => x.status === "review" && x.ci?.tone === "fail");
|
|
814
|
+
if (l) return { icon: "\u274C", text: fmt(l, "CI \u0443\u043F\u0430\u043B \u2014 \u0447\u0438\u043D\u0438\u0442\u044C"), tone: "fail" };
|
|
815
|
+
l = by((x) => x.status === "blocked");
|
|
816
|
+
if (l) return { icon: "\u2753", text: fmt(l, "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430"), tone: "blocked" };
|
|
817
|
+
l = by((x) => x.status === "stalled");
|
|
818
|
+
if (l) return { icon: "\u26A0\uFE0F", text: fmt(l, "stalled \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430"), tone: "stalled" };
|
|
819
|
+
l = by((x) => x.status === "review" && x.blocker.tone === "merge");
|
|
820
|
+
if (l) return { icon: "\u{1F535}", text: fmt(l, "CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u043C\u0435\u0440\u0436\u0430"), tone: "merge" };
|
|
821
|
+
l = by((x) => x.status === "queued" && x.dependsOn.some((d) => !d.met));
|
|
822
|
+
if (l) {
|
|
823
|
+
const dep = l.dependsOn.find((d) => !d.met);
|
|
824
|
+
return { icon: "\u26D4", text: fmt(l, `\u0436\u0434\u0451\u0442 ${dep.slug}${dep.name ? " \xB7 " + dep.name : ""}`), tone: "dep" };
|
|
825
|
+
}
|
|
826
|
+
l = by((x) => x.status === "review");
|
|
827
|
+
if (l) return { icon: "\u23F3", text: fmt(l, "review \u2014 CI \u0438\u0434\u0451\u0442"), tone: "review" };
|
|
828
|
+
l = by((x) => x.status === "running");
|
|
829
|
+
if (l) return { icon: "\u25B6", text: `${live.filter((x) => x.status === "running").length} \u0434\u0440\u043E\u043D\u0430 \u0432 \u0440\u0430\u0431\u043E\u0442\u0435 \u2014 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043E\u043A \u043D\u0435\u0442`, tone: "run" };
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ../swarm-core/src/status.ts
|
|
834
|
+
var STATUS_META = {
|
|
835
|
+
queued: { pill: "queued", label: "\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438", color: "--muted-foreground" },
|
|
836
|
+
running: { pill: "run", label: "\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", color: "--primary" },
|
|
837
|
+
blocked: { pill: "blocked", label: "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430", color: "--warning" },
|
|
838
|
+
paused: { pill: "paused", label: "\u0437\u0430\u043C\u0435\u0440", color: "--warning" },
|
|
839
|
+
// overmind был недоступен — ждёт пробуждения (TASWM-41)
|
|
840
|
+
review: { pill: "review", label: "\u043D\u0430 \u0440\u0435\u0432\u044C\u044E", color: "--info" },
|
|
841
|
+
done: { pill: "done", label: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", color: "--success" },
|
|
842
|
+
failed: { pill: "failed", label: "\u0443\u043F\u0430\u043B", color: "--destructive" },
|
|
843
|
+
stalled: { pill: "stalled", label: "\u043F\u043E\u0433\u0438\u0431/\u0437\u0430\u0432\u0438\u0441", color: "--destructive" },
|
|
844
|
+
storm_dead: { pill: "storm", label: "\u0448\u0442\u043E\u0440\u043C \u2014 \u0437\u0430\u043C\u043E\u0440\u043E\u0436\u0435\u043D", color: "--warning" },
|
|
845
|
+
// умер в шторм Anthropic, поднимется после отбоя (TASWM-23)
|
|
846
|
+
stopped: { pill: "stopped", label: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", color: "--muted-foreground" },
|
|
847
|
+
superseded: { pill: "superseded", label: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D", color: "--muted-foreground" }
|
|
848
|
+
};
|
|
849
|
+
var FALLBACK = STATUS_META.queued;
|
|
850
|
+
|
|
851
|
+
// src/diff.ts
|
|
852
|
+
import { statSync } from "fs";
|
|
853
|
+
import { join as join2 } from "path";
|
|
854
|
+
import { $ } from "execa";
|
|
855
|
+
var MAX_FILES = 200;
|
|
856
|
+
var MAX_LINES_PER_FILE = 1500;
|
|
857
|
+
var MAX_UNTRACKED = 60;
|
|
858
|
+
function cleanPath(raw) {
|
|
859
|
+
let p = raw.trim();
|
|
860
|
+
if (p.startsWith('"') && p.endsWith('"') && p.length >= 2) p = p.slice(1, -1);
|
|
861
|
+
return p;
|
|
862
|
+
}
|
|
863
|
+
function stripAB(raw) {
|
|
864
|
+
const p = cleanPath(raw);
|
|
865
|
+
if (p === "/dev/null") return p;
|
|
866
|
+
if (p.startsWith("a/") || p.startsWith("b/")) return p.slice(2);
|
|
867
|
+
return p;
|
|
868
|
+
}
|
|
869
|
+
function parseUnifiedDiff(text) {
|
|
870
|
+
const files = [];
|
|
871
|
+
if (!text) return files;
|
|
872
|
+
const lines = text.split("\n");
|
|
873
|
+
let f = null;
|
|
874
|
+
let hunk = null;
|
|
875
|
+
let oldNo = 0;
|
|
876
|
+
let newNo = 0;
|
|
877
|
+
const push = () => {
|
|
878
|
+
if (f) files.push(f);
|
|
879
|
+
};
|
|
880
|
+
for (const line of lines) {
|
|
881
|
+
if (line.startsWith("diff --git ")) {
|
|
882
|
+
push();
|
|
883
|
+
hunk = null;
|
|
884
|
+
const m = line.match(/^diff --git (.+) (.+)$/);
|
|
885
|
+
const guess = m ? stripAB(m[2]) : "";
|
|
886
|
+
f = { path: guess, oldPath: null, status: "modified", added: 0, removed: 0, binary: false, hunks: [] };
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
if (!f) continue;
|
|
890
|
+
if (line.startsWith("new file mode")) {
|
|
891
|
+
f.status = "added";
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
if (line.startsWith("deleted file mode")) {
|
|
895
|
+
f.status = "deleted";
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
if (line.startsWith("rename from ") || line.startsWith("copy from ")) {
|
|
899
|
+
f.oldPath = cleanPath(line.replace(/^(rename|copy) from /, ""));
|
|
900
|
+
f.status = "renamed";
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (line.startsWith("rename to ") || line.startsWith("copy to ")) {
|
|
904
|
+
f.path = cleanPath(line.replace(/^(rename|copy) to /, ""));
|
|
905
|
+
f.status = "renamed";
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
if (line.startsWith("Binary files ")) {
|
|
909
|
+
f.binary = true;
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
if (line.startsWith("--- ")) {
|
|
913
|
+
const p = stripAB(line.slice(4));
|
|
914
|
+
if (p === "/dev/null") f.status = "added";
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (line.startsWith("+++ ")) {
|
|
918
|
+
const p = stripAB(line.slice(4));
|
|
919
|
+
if (p === "/dev/null") f.status = "deleted";
|
|
920
|
+
else if (p) f.path = p;
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
if (line.startsWith("@@")) {
|
|
924
|
+
const m = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@(.*)$/);
|
|
925
|
+
if (m) {
|
|
926
|
+
oldNo = Number(m[1]);
|
|
927
|
+
newNo = Number(m[2]);
|
|
928
|
+
hunk = { header: (m[3] ?? "").trim(), lines: [] };
|
|
929
|
+
f.hunks.push(hunk);
|
|
930
|
+
}
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (!hunk) continue;
|
|
934
|
+
if (line.startsWith("\\")) continue;
|
|
935
|
+
const tag = line[0];
|
|
936
|
+
const body = line.slice(1);
|
|
937
|
+
if (tag === "+") {
|
|
938
|
+
hunk.lines.push({ k: "add", o: null, n: newNo++, t: body });
|
|
939
|
+
f.added++;
|
|
940
|
+
} else if (tag === "-") {
|
|
941
|
+
hunk.lines.push({ k: "del", o: oldNo++, n: null, t: body });
|
|
942
|
+
f.removed++;
|
|
943
|
+
} else if (tag === " ") {
|
|
944
|
+
hunk.lines.push({ k: "ctx", o: oldNo++, n: newNo++, t: body });
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
push();
|
|
948
|
+
return files;
|
|
949
|
+
}
|
|
950
|
+
function truncateFile(f) {
|
|
951
|
+
let count = 0;
|
|
952
|
+
const hunks = [];
|
|
953
|
+
let cut = false;
|
|
954
|
+
for (const h of f.hunks) {
|
|
955
|
+
if (count >= MAX_LINES_PER_FILE) {
|
|
956
|
+
cut = true;
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
const room = MAX_LINES_PER_FILE - count;
|
|
960
|
+
if (h.lines.length <= room) {
|
|
961
|
+
hunks.push(h);
|
|
962
|
+
count += h.lines.length;
|
|
963
|
+
} else {
|
|
964
|
+
hunks.push({ header: h.header, lines: h.lines.slice(0, room) });
|
|
965
|
+
count += room;
|
|
966
|
+
cut = true;
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return cut ? { ...f, hunks, truncated: true } : f;
|
|
971
|
+
}
|
|
972
|
+
async function resolveBase(wt) {
|
|
973
|
+
const tryRef = async (ref) => {
|
|
974
|
+
const sha = (await $({ reject: false })`git -C ${wt} rev-parse --verify --quiet ${ref + "^{commit}"}`).stdout.trim();
|
|
975
|
+
if (!sha) return null;
|
|
976
|
+
const mb = (await $({ reject: false })`git -C ${wt} merge-base HEAD ${ref}`).stdout.trim();
|
|
977
|
+
return mb || null;
|
|
978
|
+
};
|
|
979
|
+
const originHead = (await $({ reject: false })`git -C ${wt} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
|
|
980
|
+
const candidates = [originHead, "origin/main", "origin/master", "main", "master"].filter(Boolean);
|
|
981
|
+
for (const c of candidates) {
|
|
982
|
+
const mb = await tryRef(c);
|
|
983
|
+
if (mb) return mb;
|
|
984
|
+
}
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
function mtime(path) {
|
|
988
|
+
try {
|
|
989
|
+
return statSync(path).mtimeMs;
|
|
990
|
+
} catch {
|
|
991
|
+
return 0;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
async function collectRunDiff(worktree, opts = {}) {
|
|
995
|
+
const stub = (reason) => ({ ok: false, reason, pr: opts.pr ?? null, files: [], added: 0, removed: 0 });
|
|
996
|
+
const gone = opts.status === "queued" ? "\u0434\u0440\u043E\u043D \u0435\u0449\u0451 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u043A\u043E\u0434\u0430 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442" : "worktree \u0443\u0431\u0440\u0430\u043D \u2014 \u043A\u043E\u0434 \u0441\u043C\u043E\u0442\u0440\u0438 \u0432 PR";
|
|
997
|
+
if (!worktree) return stub(gone);
|
|
998
|
+
const isWt = (await $({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
|
|
999
|
+
if (isWt !== "true") return stub(gone);
|
|
1000
|
+
const base2 = await resolveBase(worktree);
|
|
1001
|
+
const from = base2 ?? "HEAD";
|
|
1002
|
+
const trackedTxt = (await $({ reject: false })`git -C ${worktree} diff --no-color --find-renames ${from} --`).stdout;
|
|
1003
|
+
let files = parseUnifiedDiff(trackedTxt);
|
|
1004
|
+
const untrackedRaw = (await $({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
|
|
1005
|
+
const untracked = untrackedRaw.split("\0").filter(Boolean).slice(0, MAX_UNTRACKED);
|
|
1006
|
+
for (const rel of untracked) {
|
|
1007
|
+
const t = (await $({ reject: false })`git -C ${worktree} diff --no-color --no-index -- /dev/null ${rel}`).stdout;
|
|
1008
|
+
for (const pf of parseUnifiedDiff(t)) {
|
|
1009
|
+
pf.status = "added";
|
|
1010
|
+
if (!pf.path || pf.path === "/dev/null") pf.path = rel;
|
|
1011
|
+
files.push(pf);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
files.sort((a, b) => mtime(join2(worktree, b.path)) - mtime(join2(worktree, a.path)));
|
|
1015
|
+
const truncatedFiles = files.length > MAX_FILES;
|
|
1016
|
+
if (truncatedFiles) files = files.slice(0, MAX_FILES);
|
|
1017
|
+
const added = files.reduce((s, f) => s + f.added, 0);
|
|
1018
|
+
const removed = files.reduce((s, f) => s + f.removed, 0);
|
|
1019
|
+
const out = files.map(truncateFile);
|
|
1020
|
+
return {
|
|
1021
|
+
ok: true,
|
|
1022
|
+
base: base2 ? base2.slice(0, 9) : null,
|
|
1023
|
+
files: out,
|
|
1024
|
+
added,
|
|
1025
|
+
removed,
|
|
1026
|
+
...truncatedFiles ? { truncated: true } : {}
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/oauth.ts
|
|
1031
|
+
import { createHash, randomBytes } from "crypto";
|
|
1032
|
+
import { readFileSync as readFileSync2, writeFileSync, unlinkSync } from "fs";
|
|
1033
|
+
import { join as join3 } from "path";
|
|
1034
|
+
var REGISTER = "/api/oauth/register";
|
|
1035
|
+
var AUTHORIZE = "/api/oauth/authorize";
|
|
1036
|
+
var TOKEN = "/api/oauth/token";
|
|
1037
|
+
var SCOPE = "openid profile email taskless:full";
|
|
1038
|
+
var STORE_PATH = join3(SWARM_HOME, "oauth.json");
|
|
1039
|
+
var CLIENT_PATH = join3(SWARM_HOME, "oauth-client.json");
|
|
1040
|
+
function makePkce() {
|
|
1041
|
+
const verifier = randomBytes(48).toString("base64url");
|
|
1042
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
1043
|
+
return { verifier, challenge };
|
|
1044
|
+
}
|
|
1045
|
+
var base = (api) => api.replace(/\/$/, "");
|
|
1046
|
+
async function registerClient(args, fetchFn = fetch) {
|
|
1047
|
+
const res = await fetchFn(`${base(args.api)}${REGISTER}`, {
|
|
1048
|
+
method: "POST",
|
|
1049
|
+
headers: { "Content-Type": "application/json" },
|
|
1050
|
+
body: JSON.stringify({
|
|
1051
|
+
client_name: args.name ?? "Taskless Swarm",
|
|
1052
|
+
redirect_uris: [args.redirectUri],
|
|
1053
|
+
token_endpoint_auth_method: "none"
|
|
1054
|
+
})
|
|
1055
|
+
});
|
|
1056
|
+
if (!res.ok) throw new Error(`register: HTTP ${res.status} ${(await res.text().catch(() => "")).slice(0, 160)}`);
|
|
1057
|
+
const j = await res.json();
|
|
1058
|
+
if (!j.client_id) throw new Error("register: \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442 client_id");
|
|
1059
|
+
return j.client_id;
|
|
1060
|
+
}
|
|
1061
|
+
function authorizeUrl(args) {
|
|
1062
|
+
const u = new URL(`${base(args.api)}${AUTHORIZE}`);
|
|
1063
|
+
u.searchParams.set("response_type", "code");
|
|
1064
|
+
u.searchParams.set("client_id", args.clientId);
|
|
1065
|
+
u.searchParams.set("redirect_uri", args.redirectUri);
|
|
1066
|
+
u.searchParams.set("scope", SCOPE);
|
|
1067
|
+
u.searchParams.set("code_challenge", args.challenge);
|
|
1068
|
+
u.searchParams.set("code_challenge_method", "S256");
|
|
1069
|
+
u.searchParams.set("state", args.state);
|
|
1070
|
+
return u.toString();
|
|
1071
|
+
}
|
|
1072
|
+
async function postToken(api, params, fetchFn) {
|
|
1073
|
+
const res = await fetchFn(`${base(api)}${TOKEN}`, {
|
|
1074
|
+
method: "POST",
|
|
1075
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1076
|
+
body: new URLSearchParams(params).toString()
|
|
1077
|
+
});
|
|
1078
|
+
const text = await res.text();
|
|
1079
|
+
if (!res.ok) throw new Error(`token ${params.grant_type}: HTTP ${res.status} ${text.slice(0, 200)}`);
|
|
1080
|
+
const j = JSON.parse(text);
|
|
1081
|
+
if (!j.access_token) throw new Error("token: \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442 access_token");
|
|
1082
|
+
return j;
|
|
1083
|
+
}
|
|
1084
|
+
function exchangeCode(args, fetchFn = fetch) {
|
|
1085
|
+
return postToken(
|
|
1086
|
+
args.api,
|
|
1087
|
+
{
|
|
1088
|
+
grant_type: "authorization_code",
|
|
1089
|
+
code: args.code,
|
|
1090
|
+
code_verifier: args.verifier,
|
|
1091
|
+
client_id: args.clientId,
|
|
1092
|
+
redirect_uri: args.redirectUri
|
|
1093
|
+
},
|
|
1094
|
+
fetchFn
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
function refreshTokens(args, fetchFn = fetch) {
|
|
1098
|
+
return postToken(
|
|
1099
|
+
args.api,
|
|
1100
|
+
{ grant_type: "refresh_token", refresh_token: args.refreshToken, client_id: args.clientId },
|
|
1101
|
+
fetchFn
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
function emailFromIdToken(idToken) {
|
|
1105
|
+
const payload = idToken?.split(".")[1];
|
|
1106
|
+
if (!payload) return void 0;
|
|
1107
|
+
try {
|
|
1108
|
+
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8")).email;
|
|
1109
|
+
} catch {
|
|
1110
|
+
return void 0;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function toStore(clientId, t, now) {
|
|
1114
|
+
return {
|
|
1115
|
+
clientId,
|
|
1116
|
+
accessToken: t.access_token,
|
|
1117
|
+
refreshToken: t.refresh_token,
|
|
1118
|
+
expiresAt: now + t.expires_in * 1e3,
|
|
1119
|
+
email: emailFromIdToken(t.id_token)
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function loadStore() {
|
|
1123
|
+
try {
|
|
1124
|
+
return JSON.parse(readFileSync2(STORE_PATH, "utf8"));
|
|
1125
|
+
} catch {
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
function saveStore(s) {
|
|
1130
|
+
writeFileSync(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
|
|
1131
|
+
}
|
|
1132
|
+
function clearStore() {
|
|
1133
|
+
try {
|
|
1134
|
+
unlinkSync(STORE_PATH);
|
|
1135
|
+
} catch {
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
function loadClientId() {
|
|
1139
|
+
try {
|
|
1140
|
+
return JSON.parse(readFileSync2(CLIENT_PATH, "utf8")).clientId;
|
|
1141
|
+
} catch {
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
async function ensureClientId(api, redirectUri, fetchFn = fetch) {
|
|
1146
|
+
const existing = loadClientId();
|
|
1147
|
+
if (existing) return existing;
|
|
1148
|
+
const clientId = await registerClient({ api, redirectUri }, fetchFn);
|
|
1149
|
+
writeFileSync(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
|
|
1150
|
+
return clientId;
|
|
1151
|
+
}
|
|
1152
|
+
async function ensureAccess(store, opts = {}) {
|
|
1153
|
+
const api = opts.api ?? TASKLESS_API;
|
|
1154
|
+
const now = opts.now ?? Date.now();
|
|
1155
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
1156
|
+
if (store.expiresAt - now > 6e4) return { accessToken: store.accessToken };
|
|
1157
|
+
try {
|
|
1158
|
+
const t = await refreshTokens({ api, clientId: store.clientId, refreshToken: store.refreshToken }, fetchFn);
|
|
1159
|
+
const next = toStore(store.clientId, t, now);
|
|
1160
|
+
next.email = next.email ?? store.email;
|
|
1161
|
+
return { accessToken: next.accessToken, store: next };
|
|
1162
|
+
} catch {
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// src/web.ts
|
|
1168
|
+
var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
1169
|
+
var jsonForScript = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
|
|
1170
|
+
var TASKLESS_MARK = `<svg viewBox="0 0 60 54" width="19" height="19" fill="none" aria-hidden="true" style="display:block"><g transform="matrix(1.0512,0,0,1.0512,0,-0.021)"><path fill="currentColor" fill-rule="nonzero" d="M46.92,0 L10.24,0 C4.59,0 0,4.59 0,10.24 L0,10.4 C0,16.05 4.59,20.64 10.24,20.64 L16.84,20.64 C19.56,20.64 22.12,19.59 24.05,17.67 L36.06,5.77 C37.25,4.6 38.82,3.95 40.49,3.95 L46.93,3.95 C50.4,3.95 53.22,6.77 53.22,10.24 L53.22,10.4 C53.22,13.87 50.4,16.69 46.93,16.69 L39.82,16.69 C37.05,16.69 34.36,17.83 32.44,19.83 L21.13,31.58 C19.28,33.5 18.27,36.02 18.27,38.68 L18.27,40.94 C18.27,46.59 22.86,51.18 28.51,51.18 C34.16,51.18 38.75,46.59 38.75,40.94 L38.75,20.74 C39.1,20.68 39.46,20.64 39.82,20.64 L46.93,20.64 C52.58,20.64 57.17,16.05 57.17,10.4 L57.17,10.24 C57.17,4.59 52.58,0 46.93,0 L46.92,0 Z M21.26,14.87 C20.07,16.04 18.5,16.69 16.83,16.69 L10.23,16.69 C6.76,16.69 3.94,13.87 3.94,10.4 L3.94,10.24 C3.94,6.77 6.76,3.95 10.23,3.95 L32.27,3.95 L21.25,14.87 L21.26,14.87 Z M34.79,40.94 C34.79,44.41 31.97,47.23 28.5,47.23 C25.03,47.23 22.21,44.41 22.21,40.94 L22.21,38.68 C22.21,37.05 22.83,35.5 23.97,34.32 L34.79,23.07 L34.79,40.94 L34.79,40.94 Z"/></g></svg>`;
|
|
1171
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1172
|
+
var sqliteMs = (ts) => Date.parse(ts.replace(" ", "T") + "Z");
|
|
1173
|
+
var sqliteIso = (ts) => {
|
|
1174
|
+
const ms = sqliteMs(ts);
|
|
1175
|
+
return Number.isNaN(ms) ? ts : new Date(ms).toISOString();
|
|
1176
|
+
};
|
|
1177
|
+
function roySnapshot(db2, now = Date.now()) {
|
|
1178
|
+
const all = db2.listRuns();
|
|
1179
|
+
const c = (s) => all.filter((r) => r.status === s).length;
|
|
1180
|
+
const withQ = new Set(db2.runIdsWithQuestions());
|
|
1181
|
+
const shown = all.filter((r) => {
|
|
1182
|
+
if (r.status === "superseded") return false;
|
|
1183
|
+
if (r.status === "done" || r.status === "stopped" || r.status === "failed") return sqliteMs(r.updated_at) >= now - DAY_MS;
|
|
1184
|
+
return true;
|
|
1185
|
+
});
|
|
1186
|
+
return {
|
|
1187
|
+
metrics: {
|
|
1188
|
+
running: c("running"),
|
|
1189
|
+
blocked: c("blocked"),
|
|
1190
|
+
review: c("review"),
|
|
1191
|
+
queued: c("queued"),
|
|
1192
|
+
stalled: c("stalled"),
|
|
1193
|
+
mergedToday: db2.mergedToday()
|
|
1194
|
+
},
|
|
1195
|
+
runs: shown.map((r) => {
|
|
1196
|
+
const evs = db2.events(r.id);
|
|
1197
|
+
let step = null;
|
|
1198
|
+
let reason = null;
|
|
1199
|
+
for (const e of evs) {
|
|
1200
|
+
if (e.kind === "step") step = e.text ?? step;
|
|
1201
|
+
if (e.kind === "stalled") reason = e.text ?? reason;
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
id: r.id,
|
|
1205
|
+
slug: r.task_slug,
|
|
1206
|
+
name: r.name,
|
|
1207
|
+
repo: r.repo ?? "",
|
|
1208
|
+
area: r.area,
|
|
1209
|
+
status: r.status,
|
|
1210
|
+
pr: r.pr_url,
|
|
1211
|
+
branch: r.branch,
|
|
1212
|
+
pid: r.pid,
|
|
1213
|
+
worktree: r.worktree_path,
|
|
1214
|
+
title: r.task_title,
|
|
1215
|
+
desc: r.task_desc,
|
|
1216
|
+
taskUrl: r.task_url,
|
|
1217
|
+
stopReason: r.stop_reason,
|
|
1218
|
+
reason,
|
|
1219
|
+
updated: r.updated_at,
|
|
1220
|
+
step,
|
|
1221
|
+
hasDecision: withQ.has(r.id),
|
|
1222
|
+
rescued: evs.some((e) => e.kind === "rescued"),
|
|
1223
|
+
// worktree сохранён — есть неспасённая работа
|
|
1224
|
+
waiting: r.status === "queued" ? db2.unmetDeps(r.depends_on ?? []) : [],
|
|
1225
|
+
// spawn --after → «ждёт: TS-1, TS-2»
|
|
1226
|
+
events: evs.map((e) => ({ kind: e.kind, text: e.text ?? null, at: e.at }))
|
|
1227
|
+
};
|
|
1228
|
+
}),
|
|
1229
|
+
questions: db2.openQuestions().map((q) => {
|
|
1230
|
+
const run = all.find((r) => r.id === q.run_id);
|
|
1231
|
+
return {
|
|
1232
|
+
id: q.id,
|
|
1233
|
+
run_id: q.run_id,
|
|
1234
|
+
slug: run?.task_slug ?? "?",
|
|
1235
|
+
name: run?.name ?? `#${q.run_id}`,
|
|
1236
|
+
repo: run?.repo ?? "",
|
|
1237
|
+
text: q.text,
|
|
1238
|
+
options: q.options,
|
|
1239
|
+
escalated: q.escalated
|
|
1240
|
+
};
|
|
1241
|
+
}),
|
|
1242
|
+
history: db2.history(40),
|
|
1243
|
+
storm: {
|
|
1244
|
+
active: db2.stormActive(),
|
|
1245
|
+
since: db2.currentStormStartedAt(),
|
|
1246
|
+
dead: all.filter((r) => r.status === "storm_dead").length
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
var TL_RECENT_TERMINAL_MS = Number(process.env.ROY_WINDOW_H || 5) * 36e5;
|
|
1251
|
+
var TL_MAX_TERMINAL_LANES = Number(process.env.ROY_MAX_DONE || 6);
|
|
1252
|
+
var TL_MAX_AXIS_MS = Number(process.env.ROY_AXIS_H || 6) * 36e5;
|
|
1253
|
+
var TL_TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "stopped", "superseded"]);
|
|
1254
|
+
var ciCache = /* @__PURE__ */ new Map();
|
|
1255
|
+
var CI_TTL = 2e4;
|
|
1256
|
+
var CI_MOCK = (() => {
|
|
1257
|
+
try {
|
|
1258
|
+
return JSON.parse(process.env.ROY_CI_MOCK || "{}");
|
|
1259
|
+
} catch {
|
|
1260
|
+
return {};
|
|
1261
|
+
}
|
|
1262
|
+
})();
|
|
1263
|
+
function mockCi(bucket) {
|
|
1264
|
+
const b = bucket.toLowerCase();
|
|
1265
|
+
if (b === "fail") return { bucket: "fail", tone: "fail", text: "\u274C CI \u0443\u043F\u0430\u043B (1/2)", total: 2, pass: 1, fail: 1, pending: 0 };
|
|
1266
|
+
if (b === "pending") return { bucket: "pending", tone: "review", text: "\u23F3 CI \u0438\u0434\u0451\u0442 (1/2)", total: 2, pass: 1, fail: 0, pending: 1 };
|
|
1267
|
+
return { bucket: "pass", tone: "merge", text: "\u2705 CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u043A \u043C\u0435\u0440\u0436\u0443", total: 2, pass: 2, fail: 0, pending: 0 };
|
|
1268
|
+
}
|
|
1269
|
+
async function ghChecks(pr) {
|
|
1270
|
+
try {
|
|
1271
|
+
const { stdout: out } = await $2({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`;
|
|
1272
|
+
const rows = JSON.parse(out);
|
|
1273
|
+
if (!Array.isArray(rows) || rows.length === 0) return null;
|
|
1274
|
+
let pass = 0, fail = 0, pending2 = 0;
|
|
1275
|
+
for (const r of rows) {
|
|
1276
|
+
const b = (r.bucket || "").toLowerCase();
|
|
1277
|
+
if (b === "pass") pass++;
|
|
1278
|
+
else if (b === "fail" || b === "cancel") fail++;
|
|
1279
|
+
else if (b === "pending") pending2++;
|
|
1280
|
+
}
|
|
1281
|
+
const total = rows.length;
|
|
1282
|
+
let bucket, text, tone;
|
|
1283
|
+
if (fail > 0) {
|
|
1284
|
+
bucket = "fail";
|
|
1285
|
+
tone = "fail";
|
|
1286
|
+
text = `\u274C CI \u0443\u043F\u0430\u043B (${fail}/${total})`;
|
|
1287
|
+
} else if (pending2 > 0) {
|
|
1288
|
+
bucket = "pending";
|
|
1289
|
+
tone = "review";
|
|
1290
|
+
text = `\u23F3 CI \u0438\u0434\u0451\u0442 (${pass}/${total})`;
|
|
1291
|
+
} else {
|
|
1292
|
+
bucket = "pass";
|
|
1293
|
+
tone = "merge";
|
|
1294
|
+
text = `\u2705 CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u043A \u043C\u0435\u0440\u0436\u0443`;
|
|
1295
|
+
}
|
|
1296
|
+
return { bucket, text, tone, total, pass, fail, pending: pending2 };
|
|
1297
|
+
} catch {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
async function refreshCI(prs) {
|
|
1302
|
+
await Promise.all(prs.map(async (pr) => {
|
|
1303
|
+
const ci = await ghChecks(pr);
|
|
1304
|
+
ciCache.set(pr, { ci, at: Date.now() });
|
|
1305
|
+
}));
|
|
1306
|
+
}
|
|
1307
|
+
function timelineState(db2, now = Date.now()) {
|
|
1308
|
+
const runs = db2.listRuns().slice().sort((a, b) => a.id - b.id);
|
|
1309
|
+
const doneNameBySlug = /* @__PURE__ */ new Map();
|
|
1310
|
+
const activeBySlug = /* @__PURE__ */ new Map();
|
|
1311
|
+
for (const r of runs) {
|
|
1312
|
+
if (!TL_TERMINAL.has(r.status)) activeBySlug.set(r.task_slug, { id: r.id, name: r.name });
|
|
1313
|
+
if (r.status === "done") doneNameBySlug.set(r.task_slug, r.name);
|
|
1314
|
+
}
|
|
1315
|
+
const openQ = /* @__PURE__ */ new Map();
|
|
1316
|
+
for (const q of db2.openQuestions()) openQ.set(q.run_id, q.text);
|
|
1317
|
+
const cutoff = now - TL_RECENT_TERMINAL_MS;
|
|
1318
|
+
const nowIso = new Date(now).toISOString();
|
|
1319
|
+
const lanes = [];
|
|
1320
|
+
const reviewPRs = [];
|
|
1321
|
+
for (const r of runs) {
|
|
1322
|
+
const live = !TL_TERMINAL.has(r.status);
|
|
1323
|
+
const recencyGated = !live || r.status === "stalled";
|
|
1324
|
+
if (recencyGated && sqliteMs(r.updated_at) < cutoff) continue;
|
|
1325
|
+
const events = db2.events(r.id).map((e) => ({ kind: e.kind, text: e.text ?? null, at: sqliteIso(e.at) }));
|
|
1326
|
+
const steps = events.filter((e) => e.kind === "step" && e.text).map((e) => ({ at: e.at, text: e.text }));
|
|
1327
|
+
const lastStep = steps.length ? steps[steps.length - 1].text : null;
|
|
1328
|
+
const laneEnd = live ? nowIso : sqliteIso(r.updated_at);
|
|
1329
|
+
const segments = buildSegments(sqliteIso(r.created_at), events, laneEnd);
|
|
1330
|
+
const deps = (r.depends_on ?? []).map((slug) => {
|
|
1331
|
+
const tgt = activeBySlug.get(slug);
|
|
1332
|
+
return { slug, name: tgt?.name ?? null, runId: tgt?.id ?? null, met: doneNameBySlug.has(slug) };
|
|
1333
|
+
});
|
|
1334
|
+
let ci = null;
|
|
1335
|
+
if (r.status === "review" && r.pr_url) {
|
|
1336
|
+
const mock = CI_MOCK[r.pr_url];
|
|
1337
|
+
if (mock) ci = mockCi(mock);
|
|
1338
|
+
else {
|
|
1339
|
+
reviewPRs.push(r.pr_url);
|
|
1340
|
+
ci = ciCache.get(r.pr_url)?.ci ?? null;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const obsoleteBy = live && doneNameBySlug.has(r.task_slug) ? doneNameBySlug.get(r.task_slug) : null;
|
|
1344
|
+
const obsolete = obsoleteBy != null;
|
|
1345
|
+
const blocker = obsolete ? { icon: "\u{1F9DF}", text: `\u0443\u0441\u0442\u0430\u0440\u0435\u043B \u2014 \u0437\u0430\u0434\u0430\u0447\u0443 \u0437\u0430\u043A\u0440\u044B\u043B ${obsoleteBy}`, tone: "queued" } : computeBlocker(r.status, r.stop_reason, lastStep, r.pr_url, ci, deps, openQ.get(r.id) ?? null);
|
|
1346
|
+
lanes.push({
|
|
1347
|
+
id: r.id,
|
|
1348
|
+
name: r.name,
|
|
1349
|
+
slug: r.task_slug,
|
|
1350
|
+
repo: basename(r.repo || "") || "?",
|
|
1351
|
+
status: r.status,
|
|
1352
|
+
area: r.area,
|
|
1353
|
+
start: sqliteIso(r.created_at),
|
|
1354
|
+
end: laneEnd,
|
|
1355
|
+
live,
|
|
1356
|
+
obsolete,
|
|
1357
|
+
obsoleteBy,
|
|
1358
|
+
segments,
|
|
1359
|
+
steps,
|
|
1360
|
+
blocker,
|
|
1361
|
+
ci,
|
|
1362
|
+
pr: r.pr_url,
|
|
1363
|
+
dependsOn: deps,
|
|
1364
|
+
cost: r.cost_usd || 0
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
const liveLanes = lanes.filter((l) => l.live);
|
|
1368
|
+
const doneLanes = lanes.filter((l) => !l.live).sort((a, b) => a.end > b.end ? -1 : a.end < b.end ? 1 : 0).slice(0, TL_MAX_TERMINAL_LANES);
|
|
1369
|
+
const shown = [...liveLanes, ...doneLanes];
|
|
1370
|
+
const tier = (l) => l.obsolete ? 1 : l.live ? 0 : 2;
|
|
1371
|
+
shown.sort((a, b) => {
|
|
1372
|
+
const ta = tier(a), tb = tier(b);
|
|
1373
|
+
if (ta !== tb) return ta - tb;
|
|
1374
|
+
return a.start < b.start ? -1 : a.start > b.start ? 1 : 0;
|
|
1375
|
+
});
|
|
1376
|
+
const earliest = shown.length ? shown.reduce((m, l) => l.start < m ? l.start : m, shown[0].start) : nowIso;
|
|
1377
|
+
const capMs = now - TL_MAX_AXIS_MS;
|
|
1378
|
+
const axisStart = Date.parse(earliest) < capMs ? new Date(capMs).toISOString() : earliest;
|
|
1379
|
+
const c = { running: 0, waitingCI: 0, waitingMerge: 0, waitingDep: 0, stalled: 0, blocked: 0 };
|
|
1380
|
+
for (const l of shown) {
|
|
1381
|
+
if (!l.live || l.obsolete) continue;
|
|
1382
|
+
if (l.status === "running") c.running++;
|
|
1383
|
+
else if (l.status === "blocked") c.blocked++;
|
|
1384
|
+
else if (l.status === "stalled") c.stalled++;
|
|
1385
|
+
else if (l.status === "queued" && l.dependsOn.some((d) => !d.met)) c.waitingDep++;
|
|
1386
|
+
else if (l.status === "review") {
|
|
1387
|
+
if (l.blocker.tone === "merge") c.waitingMerge++;
|
|
1388
|
+
else c.waitingCI++;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
const headline = pickHeadline(shown);
|
|
1392
|
+
const stale = [...new Set(reviewPRs)].filter((pr) => now - (ciCache.get(pr)?.at ?? 0) > CI_TTL);
|
|
1393
|
+
if (stale.length) void refreshCI(stale);
|
|
1394
|
+
return { now: nowIso, axisStart, axisEnd: nowIso, counters: c, headline, lanes: shown };
|
|
1395
|
+
}
|
|
1396
|
+
var CSS = `
|
|
1397
|
+
:root {
|
|
1398
|
+
--background: oklch(0.155 0.016 250); --foreground: oklch(0.93 0.006 250);
|
|
1399
|
+
--card: oklch(0.185 0.018 250); --card-border: oklch(0.20 0.014 250);
|
|
1400
|
+
--card-grad-bright: oklch(0.19 0.018 250); --card-border-bright: oklch(0.22 0.014 250); --card-border-dim: oklch(0.17 0.014 250);
|
|
1401
|
+
--popover: oklch(0.185 0.018 250); --secondary: oklch(0.19 0.018 250);
|
|
1402
|
+
--muted-foreground: oklch(0.60 0.012 250); --border: oklch(0.24 0.013 250); --input: oklch(0.30 0.014 250);
|
|
1403
|
+
--rule: oklch(0.22 0.014 250); --sidebar: oklch(0.135 0.014 250); --sidebar-accent: oklch(0.19 0.018 250);
|
|
1404
|
+
--primary: oklch(0.70 0.19 300); --primary-soft: oklch(0.70 0.19 300 / 0.15); --primary-foreground: oklch(0.98 0.02 300); --ring: oklch(0.70 0.19 300 / 0.3);
|
|
1405
|
+
--destructive: oklch(0.62 0.19 25); --success: oklch(0.72 0.15 150); --warning: oklch(0.78 0.155 75); --info: oklch(0.72 0.17 255);
|
|
1406
|
+
--shadow-card: 0 1px 2px rgba(0,0,0,.3), 0 4px 12px rgba(0,0,0,.25);
|
|
1407
|
+
--radius-sm: 2px; --radius-md: 3px; --radius-lg: 5px; --radius-xl: 7px; --radius-modal: 10px; --radius-full: 999px; --scroll-fade-size: 32px;
|
|
1408
|
+
--font-sans: 'Figtree', system-ui, sans-serif; --font-heading: 'Barlow Semi Condensed', system-ui, sans-serif; --font-mono: 'Barlow', ui-monospace, monospace;
|
|
1409
|
+
/* \u0442\u043E\u043D\u0430 \u0444\u0430\u0437/\u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432 \u0442\u0430\u0439\u043C\u043B\u0430\u0439\u043D\u0430 \u2014 \u0441\u0441\u044B\u043B\u043A\u0438 \u043D\u0430 \u0431\u0430\u0437\u043E\u0432\u044B\u0435 \u0442\u043E\u043A\u0435\u043D\u044B (\u0440\u0435\u0437\u043E\u043B\u0432\u044F\u0442\u0441\u044F \u0432 \u043E\u0431\u0435\u0438\u0445 \u0442\u0435\u043C\u0430\u0445) */
|
|
1410
|
+
--c-run: var(--primary); --c-review: var(--info); --c-merge: var(--success); --c-done: var(--success);
|
|
1411
|
+
--c-dep: var(--warning); --c-stalled: var(--destructive); --c-fail: var(--destructive); --c-blocked: var(--warning); --c-queued: var(--muted-foreground);
|
|
1412
|
+
}
|
|
1413
|
+
@media (prefers-color-scheme: light) {
|
|
1414
|
+
:root {
|
|
1415
|
+
--background: oklch(0.985 0.003 250); --foreground: oklch(0.23 0.02 260);
|
|
1416
|
+
--card: oklch(1 0 0); --card-border: oklch(0.91 0.006 250);
|
|
1417
|
+
--card-grad-bright: oklch(0.995 0.004 250); --card-border-bright: oklch(0.88 0.01 250); --card-border-dim: oklch(0.94 0.006 250);
|
|
1418
|
+
--popover: oklch(1 0 0); --secondary: oklch(0.965 0.005 250);
|
|
1419
|
+
--muted-foreground: oklch(0.50 0.02 258); --border: oklch(0.90 0.006 250); --input: oklch(0.84 0.012 250);
|
|
1420
|
+
--rule: oklch(0.925 0.006 250); --sidebar: oklch(0.965 0.005 255); --sidebar-accent: oklch(0.93 0.008 260);
|
|
1421
|
+
--primary: oklch(0.55 0.24 300); --primary-soft: oklch(0.55 0.24 300 / 0.12); --primary-foreground: oklch(0.99 0.01 300); --ring: oklch(0.55 0.24 300 / 0.35);
|
|
1422
|
+
--destructive: oklch(0.55 0.21 25); --success: oklch(0.52 0.15 150); --warning: oklch(0.62 0.15 70); --info: oklch(0.52 0.18 255);
|
|
1423
|
+
--shadow-card: 0 1px 2px rgba(0,0,0,.06), 0 4px 12px rgba(0,0,0,.05);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
* { box-sizing: border-box; }
|
|
1427
|
+
html, body { height: 100%; }
|
|
1428
|
+
body { margin: 0; overflow: hidden; background: var(--sidebar); color: var(--foreground); font-family: var(--font-sans); font-size: 14px; line-height: 1.45; -webkit-font-smoothing: antialiased; }
|
|
1429
|
+
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
1430
|
+
a { color: inherit; }
|
|
1431
|
+
.shell { height: 100vh; height: 100dvh; display: grid; grid-template-columns: 280px 1fr; overflow: hidden; }
|
|
1432
|
+
.sb { display: flex; flex-direction: column; padding: 12px 10px 10px; gap: 2px; min-height: 0; }
|
|
1433
|
+
.sb-head { display: flex; align-items: center; gap: 10px; padding: 4px 10px 14px; }
|
|
1434
|
+
.sb-glyph { width: 27px; height: 27px; color: var(--primary); flex: none; }
|
|
1435
|
+
.sb-title { font-family: var(--font-heading); font-size: 17px; font-weight: 700; letter-spacing: .13em; }
|
|
1436
|
+
.sb-sub { font-size: 10.5px; color: var(--muted-foreground); letter-spacing: .03em; margin-top: -1px; }
|
|
1437
|
+
.sb-sub.off { color: var(--warning); }
|
|
1438
|
+
.sb a.item { display: flex; align-items: center; gap: 10px; padding: 7px 10px; border-radius: var(--radius-lg); color: color-mix(in oklch, var(--foreground) 72%, transparent); text-decoration: none; font-size: 13.5px; cursor: pointer; outline: none; transition: background .12s, color .12s; }
|
|
1439
|
+
.sb a.item:hover { background: var(--sidebar-accent); color: var(--foreground); }
|
|
1440
|
+
.sb a.item.active { background: var(--sidebar-accent); color: var(--foreground); font-weight: 500; }
|
|
1441
|
+
.sb a.item:focus-visible { box-shadow: 0 0 0 1px var(--ring); }
|
|
1442
|
+
.sb a.item svg { width: 17px; height: 17px; flex: none; }
|
|
1443
|
+
.sb a.item.active svg .acc { stroke: var(--primary); } .sb a.item.active svg .accf { fill: var(--primary); }
|
|
1444
|
+
.sb a.item .lbl { flex: 1; }
|
|
1445
|
+
.badge { display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; padding: 0 6px; border-radius: var(--radius-full); font-size: 10px; font-weight: 600; font-variant-numeric: tabular-nums; background: var(--primary-soft); color: var(--primary); }
|
|
1446
|
+
.badge.warn { background: color-mix(in oklch, var(--warning) 15%, transparent); color: var(--warning); }
|
|
1447
|
+
.badge:empty { display: none; }
|
|
1448
|
+
.sb-foot { margin-top: auto; padding: 10px 10px 4px; display: flex; align-items: center; gap: 9px; }
|
|
1449
|
+
.ws-dot { width: 24px; height: 24px; border-radius: var(--radius-lg); background: oklch(0.58 0.20 300); display: grid; place-items: center; font-size: 11px; font-weight: 700; color: var(--primary-foreground); flex: none; }
|
|
1450
|
+
.sb-foot .who { font-size: 12px; color: var(--muted-foreground); line-height: 1.3; min-width: 0; }
|
|
1451
|
+
.sb-foot .who b { color: var(--foreground); font-weight: 500; display: block; overflow: hidden; text-overflow: ellipsis; }
|
|
1452
|
+
.sb-foot .who a { color: var(--muted-foreground); text-decoration: none; } .sb-foot .who a:hover { color: var(--foreground); }
|
|
1453
|
+
.content-outer { padding: 12px 12px 12px 0; min-height: 0; display: flex; flex-direction: column; }
|
|
1454
|
+
/* \u0428\u0442\u043E\u0440\u043C\u043E\u0432\u043E\u0439 \u0431\u0430\u043D\u043D\u0435\u0440 (TASWM-23): \u043F\u0430\u0434\u0435\u043D\u0438\u0435 Anthropic \u2014 \u0440\u043E\u0439 \u043D\u0430 \u043F\u0430\u0443\u0437\u0435. \u041A\u0440\u0430\u0441\u043D\u044B\u0439, \u043D\u0430\u0434 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u043E\u0439. */
|
|
1455
|
+
.storm-banner { flex: none; margin: 0 0 12px; padding: 12px 16px; border-radius: 10px; display: flex; align-items: center; gap: 13px; background: color-mix(in oklch, var(--destructive) 14%, var(--card)); border: 1px solid color-mix(in oklch, var(--destructive) 42%, transparent); box-shadow: var(--shadow-card); }
|
|
1456
|
+
.storm-banner[hidden] { display: none; }
|
|
1457
|
+
.storm-banner .sb-ico { width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center; background: var(--destructive); color: var(--primary-foreground); flex: none; font-size: 15px; animation: pulse 1.6s infinite; }
|
|
1458
|
+
.storm-banner .sb-body { min-width: 0; }
|
|
1459
|
+
.storm-banner .sb-t { font-weight: 700; font-size: 13.5px; color: var(--foreground); letter-spacing: .01em; }
|
|
1460
|
+
.storm-banner .sb-d { font-size: 12px; color: var(--muted-foreground); margin-top: 1px; }
|
|
1461
|
+
.content-card { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; border-radius: 10px; border: 1px solid transparent; background: radial-gradient(ellipse 80% 60% at 0% 0%, var(--card-grad-bright), var(--background) 70%) padding-box, linear-gradient(135deg, var(--card-border-bright), var(--card-border-dim) 55%) border-box; box-shadow: var(--shadow-card); }
|
|
1462
|
+
.sf { position: relative; min-height: 0; min-width: 0; display: flex; flex-direction: column; flex: 1; }
|
|
1463
|
+
.sf::before, .sf::after { content: ""; position: absolute; left: 0; right: 0; height: var(--scroll-fade-size); pointer-events: none; z-index: 2; opacity: 0; transition: opacity 180ms ease-out; }
|
|
1464
|
+
.sf::before { top: 0; background: linear-gradient(180deg, var(--background) 0%, transparent 100%); } .sf::after { bottom: 0; background: linear-gradient(0deg, var(--background) 0%, transparent 100%); }
|
|
1465
|
+
.sf[data-can-up="1"]::before { opacity: 1; } .sf[data-can-down="1"]::after { opacity: 1; }
|
|
1466
|
+
.sf-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 18px 26px 40px; }
|
|
1467
|
+
.sf-scroll::-webkit-scrollbar { width: 6px; height: 6px; } .sf-scroll::-webkit-scrollbar-thumb { background: oklch(0.5 0 0 / 0.25); border-radius: 3px; } .sf-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
1468
|
+
main { max-width: 1080px; margin: 0 auto; width: 100%; }
|
|
1469
|
+
/* Sticky-\u0448\u0430\u043F\u043A\u0430 contentCard (\u043F\u043E AppShell Taskless): \u0444\u0438\u0440\u043C\u0435\u043D\u043D\u0430\u044F \u0438\u043A\u043E\u043D\u043A\u0430 \u0440\u0430\u0437\u0434\u0435\u043B\u0430 + \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A
|
|
1470
|
+
\u0441\u043B\u0435\u0432\u0430, \u043A\u043D\u043E\u043F\u043A\u0438 \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u0441\u043F\u0440\u0430\u0432\u0430. flex:none \u2014 \u0441\u0438\u0434\u0438\u0442 \u041D\u0410\u0414 ScrollFade (.sf) \u0438 \u043D\u0435 \u0441\u043A\u0440\u043E\u043B\u043B\u0438\u0442\u0441\u044F;
|
|
1471
|
+
\u0441\u043A\u0440\u043E\u043B\u043B\u0438\u0442\u0441\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u0441\u043F\u0438\u0441\u043E\u043A \u043F\u043E\u0434 \u043D\u0435\u0439. \u0411\u0435\u0437 border-bottom: \u043F\u043E \u0433\u0430\u0439\u0434\u043B\u0430\u0439\u043D\u0443 uikit \u0440\u0430\u0437\u0434\u0435\u043B\u0438\u0442\u0435\u043B\u044F
|
|
1472
|
+
\u043C\u0435\u0436\u0434\u0443 \u0448\u0430\u043F\u043A\u043E\u0439 \u0438 \u043A\u043E\u043D\u0442\u0435\u043D\u0442\u043E\u043C \u043D\u0435\u0442 \u2014 \u0448\u0430\u043F\u043A\u0430 \u043F\u0435\u0440\u0435\u0442\u0435\u043A\u0430\u0435\u0442 \u0432 \u043A\u043E\u043D\u0442\u0435\u043D\u0442, \u0433\u0440\u0430\u043D\u0438\u0446\u0443 \u0434\u0430\u0451\u0442 ScrollFade. */
|
|
1473
|
+
.card-head { flex: none; display: flex; flex-direction: column; gap: 11px; padding: 15px 26px; }
|
|
1474
|
+
.ch-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; row-gap: 8px; }
|
|
1475
|
+
.ch-ico { width: 34px; height: 34px; border-radius: var(--radius-lg); background: var(--primary-soft); color: var(--primary); display: grid; place-items: center; flex: none; }
|
|
1476
|
+
.ch-ico svg { width: 19px; height: 19px; }
|
|
1477
|
+
.ch-tt { min-width: 0; display: flex; flex-direction: column; }
|
|
1478
|
+
.ch-tt h1 { font-family: var(--font-heading); font-size: 21px; font-weight: 600; margin: 0; letter-spacing: .02em; line-height: 1.15; }
|
|
1479
|
+
.ch-tt .sub { font-size: 12.5px; color: var(--muted-foreground); margin-top: 1px; } .ch-tt .sub b { color: var(--foreground); font-weight: 600; }
|
|
1480
|
+
.ch-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
|
|
1481
|
+
.ch-tools { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
1482
|
+
.ch-btn { font: inherit; font-size: 12.5px; font-weight: 600; padding: 6px 13px; border-radius: var(--radius-lg); border: 1px solid var(--input); background: var(--secondary); color: var(--foreground); cursor: pointer; white-space: nowrap; }
|
|
1483
|
+
.ch-btn:hover { border-color: var(--primary); color: var(--primary); } .ch-btn.on { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); }
|
|
1484
|
+
.need { font: inherit; font-size: 12px; font-weight: 600; padding: 6px 12px; border-radius: var(--radius-full); border: 1px solid color-mix(in oklch, var(--warning) 40%, transparent); background: color-mix(in oklch, var(--warning) 14%, transparent); color: var(--warning); cursor: pointer; white-space: nowrap; }
|
|
1485
|
+
.need:hover { background: color-mix(in oklch, var(--warning) 22%, transparent); }
|
|
1486
|
+
/* \u0421\u0435\u0433\u043C\u0435\u043D\u0442 \u0410\u043A\u0442\u0438\u0432\u043D\u044B\u0435|\u0417\u0430\u0432\u0435\u0440\u0448\u0451\u043D\u043D\u044B\u0435 (\u0441\u0442\u0438\u043B\u044C ui/tabs Taskless: pill-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440, active = card-fill) */
|
|
1487
|
+
.seg { display: inline-flex; align-items: center; gap: 2px; padding: 2px; border-radius: var(--radius-full); border: 1px solid var(--border); background: var(--background); width: fit-content; }
|
|
1488
|
+
.seg button { font: inherit; font-size: 12.5px; font-weight: 600; padding: 5px 13px; border-radius: var(--radius-full); border: 0; background: transparent; color: var(--muted-foreground); cursor: pointer; display: inline-flex; align-items: center; gap: 6px; transition: background .12s, color .12s; }
|
|
1489
|
+
.seg button:hover:not(.on) { color: var(--foreground); }
|
|
1490
|
+
.seg button.on { background: var(--card); color: var(--foreground); box-shadow: 0 1px 2px rgba(0,0,0,.18); }
|
|
1491
|
+
.seg button .n { font-variant-numeric: tabular-nums; font-size: 11px; opacity: .75; } .seg button.on .n { color: var(--primary); opacity: 1; }
|
|
1492
|
+
.spawnpanel { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 16px; padding: 12px 14px; border-radius: var(--radius-lg); background: var(--secondary); border: 1px solid var(--rule); }
|
|
1493
|
+
.spawnpanel input { font: inherit; font-size: 13px; padding: 7px 11px; border-radius: var(--radius-lg); border: 1px solid var(--input); background: var(--background); color: var(--foreground); min-width: 0; }
|
|
1494
|
+
.spawnpanel input:focus-visible { outline: none; box-shadow: 0 0 0 1px var(--ring); border-color: var(--primary); }
|
|
1495
|
+
.live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--primary); display: inline-block; animation: pulse 1.8s infinite; margin-right: 6px; vertical-align: 1px; }
|
|
1496
|
+
.card { background: var(--card); border: 1px solid var(--card-border); border-radius: var(--radius-xl); }
|
|
1497
|
+
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 22px; }
|
|
1498
|
+
.stat { padding: 14px 16px; } .stat .k { font-size: 11.5px; color: var(--muted-foreground); letter-spacing: .04em; }
|
|
1499
|
+
.stat .v { font-family: var(--font-heading); font-size: 30px; font-weight: 700; font-variant-numeric: tabular-nums; margin-top: 2px; line-height: 1.1; }
|
|
1500
|
+
.stat .v small { font-size: 13px; font-weight: 500; color: var(--muted-foreground); margin-left: 5px; }
|
|
1501
|
+
.stat.hot .v { color: var(--primary); } .stat.warn .v { color: var(--warning); }
|
|
1502
|
+
.sec-h { display: flex; align-items: center; gap: 9px; margin: 20px 0 10px; }
|
|
1503
|
+
.sec-h h2 { font-family: var(--font-heading); font-size: 14.5px; font-weight: 600; margin: 0; letter-spacing: .04em; text-transform: uppercase; color: var(--muted-foreground); }
|
|
1504
|
+
.sec-h .rule-ln { flex: 1; height: 1px; background: var(--rule); }
|
|
1505
|
+
.feed { padding: 4px 0; }
|
|
1506
|
+
.feed-row { display: grid; grid-template-columns: 96px 118px 1fr; gap: 12px; padding: 9px 16px; border-bottom: 1px solid var(--card-border); font-size: 13px; align-items: baseline; }
|
|
1507
|
+
.feed-row:last-child { border-bottom: 0; }
|
|
1508
|
+
.feed-row .t { color: var(--muted-foreground); font-size: 11.5px; white-space: nowrap; } .feed-row .who { font-family: var(--font-heading); font-weight: 600; font-size: 12.5px; letter-spacing: .05em; }
|
|
1509
|
+
.feed-row .what { color: color-mix(in oklch, var(--foreground) 85%, transparent); overflow: hidden; text-overflow: ellipsis; }
|
|
1510
|
+
.repo-tag { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; padding: 2px 7px; border-radius: var(--radius-md); background: var(--secondary); color: var(--muted-foreground); border: 1px solid var(--rule); white-space: nowrap; }
|
|
1511
|
+
.repofilter { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
|
|
1512
|
+
.rf { font: inherit; font-size: 12px; padding: 3px 12px; border-radius: var(--radius-full); border: 1px solid var(--rule); background: var(--secondary); color: var(--muted-foreground); cursor: pointer; }
|
|
1513
|
+
.rf:hover { border-color: var(--input); } .rf.on { color: var(--primary); border-color: var(--primary); background: var(--primary-soft); }
|
|
1514
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 10px; }
|
|
1515
|
+
.drone { min-width: 0; text-align: left; font: inherit; color: inherit; cursor: pointer; background: var(--card); border: 1px solid var(--card-border); border-radius: var(--radius-xl); padding: 14px 15px 12px; display: flex; flex-direction: column; gap: 9px; transition: border-color .12s; } /* min-width:0 \u2014 grid-\u044D\u043B\u0435\u043C\u0435\u043D\u0442 \u0443\u0436\u0438\u043C\u0430\u0435\u0442\u0441\u044F \u0434\u043E \u0442\u0440\u0435\u043A\u0430, \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u043D\u0435 \u0440\u0430\u0441\u043F\u0438\u0440\u0430\u0435\u0442 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0443 */
|
|
1516
|
+
.drone:hover { border-color: var(--input); } .drone:focus-visible { outline: none; box-shadow: 0 0 0 1px var(--ring); }
|
|
1517
|
+
.d-top { display: flex; align-items: center; gap: 10px; }
|
|
1518
|
+
.d-top > span:not(.ava):not(.pill) { min-width: 0; flex: 1; } /* \u0441\u0435\u0440\u0435\u0434\u0438\u043D\u0430 \u0443\u0436\u0438\u043C\u0430\u0435\u0442\u0441\u044F \u2014 \u043F\u0438\u043B\u044E\u043B\u044F \u043D\u0435 \u0443\u0435\u0437\u0436\u0430\u0435\u0442 \u0437\u0430 \u043A\u0440\u0430\u0439 (\u043C\u043E\u0431\u0438\u043B\u043A\u0430) */
|
|
1519
|
+
.ava { width: 34px; height: 34px; border-radius: var(--radius-lg); background: var(--secondary); border: 1px solid var(--rule); display: grid; place-items: center; flex: none; } .ava svg { width: 19px; height: 19px; }
|
|
1520
|
+
.d-name { font-family: var(--font-heading); font-weight: 700; font-size: 14.5px; letter-spacing: .07em; }
|
|
1521
|
+
.d-ids { font-family: var(--font-mono); font-size: 11.5px; color: var(--muted-foreground); display: flex; gap: 8px; margin-top: 1px; }
|
|
1522
|
+
.d-title { font-size: 13px; color: color-mix(in oklch, var(--foreground) 88%, transparent); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; min-height: 2.6em; }
|
|
1523
|
+
.d-step { font-size: 12px; color: var(--muted-foreground); display: flex; gap: 7px; align-items: baseline; border-top: 1px solid var(--card-border); padding-top: 9px; }
|
|
1524
|
+
.d-step .c { font-family: var(--font-mono); flex: none; } .d-step .txt { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1525
|
+
.d-name, .d-ids { min-width: 0; } .d-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .d-ids span { overflow: hidden; text-overflow: ellipsis; }
|
|
1526
|
+
.d-rescue { font-size: 11px; font-weight: 600; color: var(--destructive); background: color-mix(in oklch, var(--destructive) 14%, transparent); padding: 3px 9px; border-radius: var(--radius-full); align-self: flex-start; white-space: nowrap; }
|
|
1527
|
+
.d-foot { display: flex; align-items: center; gap: 8px; } .d-foot .time { margin-left: auto; font-size: 11px; color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
|
|
1528
|
+
.pill { display: inline-flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 600; padding: 2.5px 9px; border-radius: var(--radius-full); letter-spacing: .02em; margin-left: auto; white-space: nowrap; }
|
|
1529
|
+
.pill .sd { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
1530
|
+
.pill.run { color: var(--primary); background: var(--primary-soft); } .pill.run .sd { animation: pulse 1.8s infinite; }
|
|
1531
|
+
.pill.review { color: var(--info); background: color-mix(in oklch, var(--info) 14%, transparent); }
|
|
1532
|
+
.pill.blocked { color: var(--warning); background: color-mix(in oklch, var(--warning) 15%, transparent); }
|
|
1533
|
+
.pill.done { color: var(--success); background: color-mix(in oklch, var(--success) 13%, transparent); }
|
|
1534
|
+
.pill.stalled, .pill.failed { color: var(--destructive); background: color-mix(in oklch, var(--destructive) 14%, transparent); }
|
|
1535
|
+
.pill.stopped, .pill.queued, .pill.superseded { color: var(--muted-foreground); background: var(--secondary); }
|
|
1536
|
+
.empty { color: var(--muted-foreground); font-size: 13px; padding: 26px 4px; text-align: center; }
|
|
1537
|
+
.q { padding: 18px; } .q + .q { margin-top: 10px; }
|
|
1538
|
+
.q-top { display: flex; gap: 10px; align-items: flex-start; }
|
|
1539
|
+
.q-meta { font-family: var(--font-mono); font-size: 12px; color: var(--muted-foreground); } .q-text { font-size: 15px; font-weight: 600; margin: 7px 0 2px; }
|
|
1540
|
+
.tier { font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; padding: 3px 8px; border-radius: var(--radius-md); white-space: nowrap; margin-left: auto; }
|
|
1541
|
+
.tier.human { color: var(--warning); background: color-mix(in oklch, var(--warning) 15%, transparent); }
|
|
1542
|
+
.tier.orc { color: var(--info); background: color-mix(in oklch, var(--info) 14%, transparent); }
|
|
1543
|
+
.opts { display: flex; flex-wrap: wrap; gap: 7px; margin: 13px 0 3px; }
|
|
1544
|
+
.opt { font: inherit; font-size: 13px; padding: 7px 13px; border-radius: var(--radius-lg); border: 1px solid var(--input); background: var(--secondary); color: var(--foreground); cursor: pointer; }
|
|
1545
|
+
.opt:hover { border-color: var(--primary); color: var(--primary); }
|
|
1546
|
+
.answer-row { display: flex; gap: 8px; margin-top: 12px; }
|
|
1547
|
+
.answer-row input { flex: 1; font: inherit; font-size: 13px; padding: 8px 12px; border-radius: var(--radius-lg); border: 1px solid var(--input); background: var(--background); color: var(--foreground); min-width: 0; }
|
|
1548
|
+
.answer-row input::placeholder { color: var(--muted-foreground); }
|
|
1549
|
+
.answer-row input:focus-visible { outline: none; box-shadow: 0 0 0 1px var(--ring); border-color: var(--primary); }
|
|
1550
|
+
.btn { font: inherit; font-size: 13px; font-weight: 600; padding: 8px 15px; border-radius: var(--radius-lg); border: 0; background: var(--primary); color: var(--primary-foreground); cursor: pointer; white-space: nowrap; }
|
|
1551
|
+
.btn:hover { filter: brightness(1.08); }
|
|
1552
|
+
.hint { font-size: 12.5px; color: var(--muted-foreground); margin: 14px 2px 0; }
|
|
1553
|
+
.spawnbar { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; align-items: center; }
|
|
1554
|
+
.spawnbar input { font: inherit; font-size: 13px; padding: 7px 11px; border-radius: var(--radius-lg); border: 1px solid var(--input); background: var(--background); color: var(--foreground); min-width: 0; }
|
|
1555
|
+
.spawnbar input:focus-visible { outline: none; box-shadow: 0 0 0 1px var(--ring); border-color: var(--primary); }
|
|
1556
|
+
/* \u0418\u0441\u0442\u043E\u0440\u0438\u044F: \u0441\u0442\u0440\u043E\u043A\u0430 = [\u0432\u0440\u0435\u043C\u044F | \u0442\u0435\u043B\u043E]. align-items:start + \u0435\u0434\u0438\u043D\u044B\u0439 \u0440\u0438\u0442\u043C: \u043E\u0442\u043D\u043E\u0441\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0435
|
|
1557
|
+
\u0432\u0440\u0435\u043C\u044F \u0438 \u043F\u0435\u0440\u0432\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430 \u0442\u0435\u043B\u0430 \u043D\u0430 \u043E\u0434\u043D\u043E\u0439 \u043B\u0438\u043D\u0438\u0438 (padding-top \u0443 \u0432\u0440\u0435\u043C\u0435\u043D\u0438 \u043F\u043E\u0434 \u0432\u044B\u0441\u043E\u0442\u0443 \u0433\u043B\u0438\u0444\u0430),
|
|
1558
|
+
\u043C\u043D\u043E\u0433\u043E\u0441\u0442\u0440\u043E\u0447\u043D\u044B\u0439 \u043A\u043E\u043D\u0442\u0435\u043D\u0442 (\u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A/\u0441\u0443\u0442\u044C) \u0442\u0435\u0447\u0451\u0442 \u043D\u0438\u0436\u0435 \u0438 \u043D\u0435 \u043B\u043E\u043C\u0430\u0435\u0442 \u0441\u0435\u0442\u043A\u0443. */
|
|
1559
|
+
.hrow { display: grid; grid-template-columns: 118px 1fr; gap: 16px; padding: 15px 16px; border-bottom: 1px solid var(--card-border); align-items: start; }
|
|
1560
|
+
.hrow:last-child { border-bottom: 0; }
|
|
1561
|
+
.htime { font-variant-numeric: tabular-nums; line-height: 1.35; padding-top: 3px; }
|
|
1562
|
+
.htime .rel { display: block; font-size: 12px; font-weight: 500; color: color-mix(in oklch, var(--foreground) 80%, transparent); white-space: nowrap; }
|
|
1563
|
+
.htime .abs { display: block; font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); margin-top: 2px; }
|
|
1564
|
+
.hbody { min-width: 0; }
|
|
1565
|
+
.hhead { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; row-gap: 4px; }
|
|
1566
|
+
.hh-ava { width: 21px; height: 21px; border-radius: var(--radius-md); background: var(--secondary); border: 1px solid var(--rule); display: grid; place-items: center; flex: none; color: var(--muted-foreground); } .hh-ava svg { width: 13px; height: 13px; }
|
|
1567
|
+
.hh-name { font-family: var(--font-heading); font-weight: 700; font-size: 12.5px; letter-spacing: .05em; }
|
|
1568
|
+
.hh-slug { font-family: var(--font-mono); font-size: 11.5px; color: var(--muted-foreground); }
|
|
1569
|
+
.hpill { font-size: 9.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; padding: 2px 7px; border-radius: var(--radius-md); white-space: nowrap; }
|
|
1570
|
+
.htitle { font-size: 13px; font-weight: 600; color: color-mix(in oklch, var(--foreground) 90%, transparent); margin-top: 6px; }
|
|
1571
|
+
.hq { font-size: 13.5px; font-weight: 600; margin-top: 5px; }
|
|
1572
|
+
.hessence { font-size: 13px; color: var(--muted-foreground); margin-top: 4px; line-height: 1.5; }
|
|
1573
|
+
.hlinks { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 9px; }
|
|
1574
|
+
.hlinks a { font-family: var(--font-mono); font-size: 11.5px; font-weight: 600; color: var(--primary); text-decoration: none; } .hlinks a:hover { text-decoration: underline; }
|
|
1575
|
+
.hopts { display: flex; flex-wrap: wrap; gap: 6px; margin: 9px 0 0; } .hopt { font: inherit; font-family: var(--font-mono); font-size: 11.5px; padding: 3px 9px; border-radius: var(--radius-md); border: 1px solid var(--rule); color: var(--muted-foreground); background: var(--secondary); }
|
|
1576
|
+
.hopt.click { cursor: pointer; } .hopt.click:hover { border-color: var(--primary); color: var(--primary); }
|
|
1577
|
+
.hopt.pick { border-color: var(--info); color: var(--info); background: color-mix(in oklch, var(--info) 10%, transparent); cursor: default; }
|
|
1578
|
+
.hcustom { display: flex; gap: 8px; margin-top: 9px; padding: 10px 12px; border-radius: var(--radius-lg); }
|
|
1579
|
+
.hcustom .who { font-size: 10px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; } .hcustom .val { font-size: 13px; margin-top: 2px; }
|
|
1580
|
+
.hcustom.orc { background: color-mix(in oklch, var(--info) 12%, transparent); border: 1px solid color-mix(in oklch, var(--info) 30%, transparent); } .hcustom.orc .who { color: var(--info); }
|
|
1581
|
+
.hcustom.human { background: color-mix(in oklch, var(--warning) 12%, transparent); border: 1px solid color-mix(in oklch, var(--warning) 30%, transparent); } .hcustom.human .who { color: var(--warning); }
|
|
1582
|
+
.hpending { font-size: 12.5px; color: var(--muted-foreground); margin-top: 9px; }
|
|
1583
|
+
.overlay { position: fixed; inset: 0; background: oklch(0.1 0.01 250 / 0.6); z-index: 21; display: none; align-items: flex-start; justify-content: center; padding: 48px 16px; overflow-y: auto; }
|
|
1584
|
+
.overlay.open { display: flex; }
|
|
1585
|
+
.modal { background: var(--popover); border: 1px solid var(--border); border-radius: var(--radius-modal); width: 100%; max-width: 600px; box-shadow: var(--shadow-card); }
|
|
1586
|
+
.m-head { display: flex; gap: 12px; padding: 18px 20px 14px; border-bottom: 1px solid var(--card-border); align-items: flex-start; }
|
|
1587
|
+
.m-name { font-family: var(--font-heading); font-size: 17px; font-weight: 700; letter-spacing: .06em; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } .m-name .pill { margin-left: 0; }
|
|
1588
|
+
.m-meta { font-family: var(--font-mono); font-size: 11.5px; color: var(--muted-foreground); margin-top: 5px; display: flex; flex-wrap: wrap; gap: 3px 12px; }
|
|
1589
|
+
.m-close { font: inherit; margin-left: auto; background: var(--secondary); border: 1px solid var(--border); color: var(--muted-foreground); width: 28px; height: 28px; border-radius: var(--radius-md); cursor: pointer; flex: none; display: grid; place-items: center; } .m-close:hover { color: var(--foreground); } .m-close svg { width: 14px; height: 14px; }
|
|
1590
|
+
.m-body { padding: 16px 20px 6px; }
|
|
1591
|
+
.taskbox { background: var(--secondary); border: 1px solid var(--rule); border-radius: var(--radius-lg); padding: 12px 14px; margin-bottom: 16px; }
|
|
1592
|
+
.taskbox a { color: var(--primary); font-family: var(--font-mono); font-size: 12px; font-weight: 600; text-decoration: none; margin-right: 12px; } .taskbox a:hover { text-decoration: underline; }
|
|
1593
|
+
.taskbox .tt { font-size: 14px; font-weight: 600; margin: 5px 0 4px; } .taskbox .desc { font-size: 12.5px; color: var(--muted-foreground); line-height: 1.5; white-space: pre-wrap; }
|
|
1594
|
+
.steps { list-style: none; margin: 0; padding: 0 0 8px; position: relative; }
|
|
1595
|
+
.steps::before { content: ""; position: absolute; left: 8px; top: 6px; bottom: 14px; width: 2px; background: var(--rule); }
|
|
1596
|
+
.step { display: grid; grid-template-columns: 18px 1fr; gap: 12px; padding-bottom: 13px; position: relative; }
|
|
1597
|
+
.step .node { width: 17px; height: 17px; border-radius: 50%; display: grid; place-items: center; background: var(--popover); z-index: 1; color: var(--success); } .step .node svg { width: 15px; height: 15px; }
|
|
1598
|
+
.step.now .node { background: var(--primary); box-shadow: 0 0 0 4px var(--primary-soft); animation: nodepulse 1.8s infinite; } .step.fail .node { color: var(--destructive); }
|
|
1599
|
+
.step .stext { font-size: 13px; } .step.now .stext { font-weight: 600; } .step.now .stext .live-lbl { color: var(--primary); font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; margin-left: 7px; }
|
|
1600
|
+
.step .stime { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); margin-top: 1px; }
|
|
1601
|
+
.m-actions { display: flex; gap: 8px; padding: 12px 20px 18px; border-top: 1px solid var(--card-border); }
|
|
1602
|
+
.abtn { font: inherit; font-size: 13px; font-weight: 500; padding: 7px 13px; border-radius: var(--radius-lg); border: 1px solid var(--input); background: var(--secondary); color: var(--foreground); cursor: pointer; }
|
|
1603
|
+
.abtn:hover { border-color: var(--primary); color: var(--primary); } .abtn.warn:hover { border-color: var(--destructive); color: var(--destructive); }
|
|
1604
|
+
.grow { flex: 1; }
|
|
1605
|
+
/* \u043C\u043E\u0434\u0430\u043B\u043A\u0430 \u0448\u0438\u0440\u0435, \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043A\u0440\u044B\u0442\u0430 \u0432\u043A\u043B\u0430\u0434\u043A\u0430 \xAB\u041A\u043E\u0434\xBB (\u0434\u0438\u0444\u0444 \u0441 \u0434\u0432\u0443\u043C\u044F \u043A\u043E\u043B\u043E\u043D\u043A\u0430\u043C\u0438) */
|
|
1606
|
+
.modal.wide { max-width: 940px; }
|
|
1607
|
+
/* \u2500\u2500 \u0432\u043A\u043B\u0430\u0434\u043A\u0438 \u043C\u043E\u0434\u0430\u043B\u043A\u0438 \xAB\u0428\u0430\u0433\u0438 | \u041A\u043E\u0434\xBB \u2500\u2500 */
|
|
1608
|
+
.m-tabs { display: flex; align-items: center; gap: 10px; margin: 0 0 14px; }
|
|
1609
|
+
.mc-badge { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: 11px; margin-left: auto; }
|
|
1610
|
+
.mc-badge .a { color: var(--success); } .mc-badge .d { color: var(--destructive); }
|
|
1611
|
+
/* \u2500\u2500 \u043F\u0430\u043D\u0435\u043B\u044C \xAB\u041A\u043E\u0434\xBB: \u0441\u043B\u0435\u0432\u0430 \u0441\u043F\u0438\u0441\u043E\u043A \u0444\u0430\u0439\u043B\u043E\u0432, \u0441\u043F\u0440\u0430\u0432\u0430 \u0434\u0438\u0444\u0444 \u2500\u2500 */
|
|
1612
|
+
.mc-wrap { display: grid; grid-template-columns: 232px 1fr; gap: 12px; align-items: stretch; }
|
|
1613
|
+
.mc-list { display: flex; flex-direction: column; gap: 2px; max-height: 56vh; overflow-y: auto; padding-right: 2px; }
|
|
1614
|
+
.mc-list::-webkit-scrollbar, .mc-diff::-webkit-scrollbar { width: 7px; height: 7px; } .mc-list::-webkit-scrollbar-thumb, .mc-diff::-webkit-scrollbar-thumb { background: oklch(0.5 0 0 / 0.25); border-radius: 3px; }
|
|
1615
|
+
.mc-file { display: flex; align-items: center; gap: 8px; text-align: left; font: inherit; width: 100%; padding: 6px 9px; border-radius: var(--radius-md); border: 1px solid transparent; background: transparent; color: var(--foreground); cursor: pointer; }
|
|
1616
|
+
.mc-file:hover { background: var(--secondary); } .mc-file.on { background: var(--secondary); border-color: var(--border); }
|
|
1617
|
+
.mc-fs { width: 15px; height: 15px; flex: none; display: grid; place-items: center; border-radius: 3px; font-family: var(--font-mono); font-size: 11px; font-weight: 700; line-height: 1; }
|
|
1618
|
+
.mc-fs.added { color: var(--success); background: color-mix(in oklch, var(--success) 15%, transparent); }
|
|
1619
|
+
.mc-fs.modified { color: var(--warning); background: color-mix(in oklch, var(--warning) 15%, transparent); }
|
|
1620
|
+
.mc-fs.deleted { color: var(--destructive); background: color-mix(in oklch, var(--destructive) 15%, transparent); }
|
|
1621
|
+
.mc-fs.renamed { color: var(--info); background: color-mix(in oklch, var(--info) 15%, transparent); }
|
|
1622
|
+
.mc-fp { flex: 1; min-width: 0; font-family: var(--font-mono); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left; }
|
|
1623
|
+
.mc-fn { flex: none; font-family: var(--font-mono); font-size: 11px; display: flex; gap: 5px; } .mc-fn .a { color: var(--success); } .mc-fn .d { color: var(--destructive); }
|
|
1624
|
+
.mc-diff { min-width: 0; max-height: 56vh; overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--background); font-family: var(--font-mono); font-size: 12px; line-height: 1.55; }
|
|
1625
|
+
.mc-diff::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
1626
|
+
.mc-hh { display: block; padding: 3px 12px; color: var(--muted-foreground); background: var(--secondary); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); font-size: 11px; white-space: pre; }
|
|
1627
|
+
.mc-hh:first-child { border-top: 0; }
|
|
1628
|
+
.mc-ln { display: grid; grid-template-columns: 40px 40px 14px 1fr; align-items: baseline; }
|
|
1629
|
+
.mc-ln .mc-no { text-align: right; padding-right: 8px; color: color-mix(in oklch, var(--muted-foreground) 70%, transparent); font-size: 10.5px; -webkit-user-select: none; user-select: none; }
|
|
1630
|
+
.mc-ln .mc-sign { text-align: center; -webkit-user-select: none; user-select: none; }
|
|
1631
|
+
.mc-ln .mc-code { white-space: pre; padding-right: 12px; overflow-wrap: normal; }
|
|
1632
|
+
.mc-ln.add { background: color-mix(in oklch, var(--success) 11%, transparent); } .mc-ln.add .mc-sign { color: var(--success); }
|
|
1633
|
+
.mc-ln.del { background: color-mix(in oklch, var(--destructive) 11%, transparent); } .mc-ln.del .mc-sign { color: var(--destructive); }
|
|
1634
|
+
.mc-ln.add .mc-no, .mc-ln.del .mc-no { color: color-mix(in oklch, var(--muted-foreground) 85%, transparent); }
|
|
1635
|
+
.mc-more { padding: 8px 12px; color: var(--muted-foreground); font-size: 11.5px; background: var(--secondary); }
|
|
1636
|
+
.mc-foot { display: flex; align-items: center; gap: 10px; margin-top: 10px; font-family: var(--font-mono); font-size: 11.5px; color: var(--muted-foreground); } .mc-foot .a { color: var(--success); } .mc-foot .d { color: var(--destructive); }
|
|
1637
|
+
.mc-empty { padding: 40px 20px; text-align: center; color: var(--muted-foreground); font-size: 13px; display: flex; flex-direction: column; align-items: center; gap: 10px; }
|
|
1638
|
+
.mc-empty .mc-prlink { color: var(--primary); font-family: var(--font-mono); font-size: 12px; font-weight: 600; text-decoration: none; } .mc-empty .mc-prlink:hover { text-decoration: underline; }
|
|
1639
|
+
/* \u043F\u043E\u0434\u0441\u0432\u0435\u0442\u043A\u0430 \u0441\u0438\u043D\u0442\u0430\u043A\u0441\u0438\u0441\u0430 (\u0438\u043D\u043B\u0430\u0439\u043D-\u0445\u0430\u0439\u043B\u0430\u0439\u0442\u0435\u0440, \u0431\u0435\u0437 CDN) \u2014 \u0442\u043E\u043A\u0435\u043D\u044B \u043D\u0430 \u0442\u043E\u043A\u0435\u043D\u0430\u0445 \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u044B */
|
|
1640
|
+
.mc-code .k { color: var(--primary); } .mc-code .s { color: var(--success); } .mc-code .c { color: var(--muted-foreground); font-style: italic; }
|
|
1641
|
+
.mc-code .n { color: var(--warning); } .mc-code .t { color: var(--info); } .mc-code .f { color: color-mix(in oklch, var(--info) 60%, var(--foreground)); }
|
|
1642
|
+
@media (max-width: 767px) {
|
|
1643
|
+
.modal.wide { max-width: 100%; }
|
|
1644
|
+
.mc-wrap { grid-template-columns: 1fr; }
|
|
1645
|
+
.mc-list { flex-direction: row; flex-wrap: nowrap; overflow-x: auto; max-height: none; gap: 6px; padding-bottom: 4px; }
|
|
1646
|
+
.mc-file { width: auto; flex: none; }
|
|
1647
|
+
.mc-file .mc-fp { direction: ltr; max-width: 130px; }
|
|
1648
|
+
.mc-diff { max-height: 50vh; }
|
|
1649
|
+
}
|
|
1650
|
+
.login-wrap { height: 100dvh; display: grid; place-items: center; padding: 24px; }
|
|
1651
|
+
.login-card { background: var(--card); border: 1px solid var(--card-border); border-radius: var(--radius-modal); max-width: 400px; width: 100%; padding: 40px 28px; text-align: center; box-shadow: var(--shadow-card); }
|
|
1652
|
+
.login-card .logo { width: 52px; height: 52px; border-radius: var(--radius-lg); background: var(--primary-soft); color: var(--primary); display: grid; place-items: center; margin: 0 auto 16px; }
|
|
1653
|
+
.login-card h1 { font-family: var(--font-heading); font-size: 19px; font-weight: 700; letter-spacing: .04em; margin: 0 0 6px; }
|
|
1654
|
+
.login-card p { font-size: 13.5px; color: var(--muted-foreground); margin: 0 auto 22px; max-width: 320px; }
|
|
1655
|
+
.btn-tl { display: inline-flex; align-items: center; gap: 9px; background: var(--primary); color: var(--primary-foreground); border: none; border-radius: var(--radius-lg); padding: 11px 20px; font-size: 14px; font-weight: 600; cursor: pointer; text-decoration: none; }
|
|
1656
|
+
.btn-tl:hover { filter: brightness(1.08); }
|
|
1657
|
+
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
|
|
1658
|
+
@keyframes nodepulse { 0%,100% { box-shadow: 0 0 0 4px var(--primary-soft); } 50% { box-shadow: 0 0 0 7px transparent; } }
|
|
1659
|
+
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
|
|
1660
|
+
@media (max-width: 767px) {
|
|
1661
|
+
.shell { display: flex; flex-direction: column; width: 100vw; overflow-x: hidden; }
|
|
1662
|
+
.sb { flex-direction: row; align-items: center; padding: 6px 10px; gap: 4px; overflow-x: auto; background: var(--card); height: 48px; flex: none; }
|
|
1663
|
+
.sb-head { padding: 0 8px 0 0; } .sb-head .sb-txt { display: none; } .sb a.item { padding: 6px 10px; white-space: nowrap; } .sb-foot { display: none; }
|
|
1664
|
+
.content-outer { padding: 0; flex: 1; } .content-card { border-radius: 0; border: none; box-shadow: none; } .sf-scroll { padding: 16px 14px 40px; }
|
|
1665
|
+
.card-head { padding: 12px 14px; gap: 9px; } .ch-tt h1 { font-size: 18px; } .ch-actions { gap: 6px; } .ch-btn, .need { font-size: 11.5px; padding: 5px 10px; } .seg button { padding: 5px 11px; }
|
|
1666
|
+
.stats { grid-template-columns: repeat(2, 1fr); } .grid { grid-template-columns: 1fr; } .feed-row { grid-template-columns: auto 1fr; } .feed-row .what { grid-column: 1 / -1; }
|
|
1667
|
+
.hrow { grid-template-columns: 66px 1fr; gap: 10px; padding: 12px; } .htime .rel { font-size: 11px; }
|
|
1668
|
+
}
|
|
1669
|
+
[hidden] { display: none !important; }
|
|
1670
|
+
|
|
1671
|
+
/* \u2500\u2500 \u0421\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \xAB\u0422\u0430\u0439\u043C\u043B\u0430\u0439\u043D\xBB (\u043F\u043E\u0440\u0442 TASWM-47). \u0412\u0441\u0451 \u043F\u043E\u0434 .tlpage \u2014 \u043F\u0435\u0440\u0435\u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D\u044B
|
|
1672
|
+
\u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u044B \u0438 \u043D\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u0435\u0442 \u0441 \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u044B\u043C\u0438 .pill/.seg/.empty \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u0430. \u2500\u2500 */
|
|
1673
|
+
.tlpage { position: relative; }
|
|
1674
|
+
/* \u0448\u0430\u043F\u043A\u0430 \xAB\u0427\u0442\u043E \u0441\u0435\u0439\u0447\u0430\u0441\xBB \u2014 \u0441\u0432\u043E\u0434\u043A\u0430-\u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0430 (\u0441\u0447\u0451\u0442\u0447\u0438\u043A\u0438 + \xAB\u0433\u043B\u0430\u0432\u043D\u043E\u0435 \u0441\u0435\u0439\u0447\u0430\u0441\xBB). \u0411\u0435\u0437 \u043B\u0438\u043D\u0438\u0438
|
|
1675
|
+
\u043F\u043E\u0434 \u0448\u0430\u043F\u043A\u043E\u0439: headline \u043E\u0442\u0434\u0435\u043B\u0451\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u043E\u0442\u0441\u0442\u0443\u043F\u043E\u043C (\u0433\u0430\u0439\u0434\u043B\u0430\u0439\u043D \u2014 \u0431\u0435\u0437 \u043B\u0438\u0448\u043D\u0435\u0433\u043E \u0440\u0430\u0437\u0434\u0435\u043B\u0438\u0442\u0435\u043B\u044F). */
|
|
1676
|
+
.tlpage .now-head { background: var(--card); border: 1px solid var(--card-border); border-radius: var(--radius-lg); padding: 14px 16px; margin-bottom: 18px; }
|
|
1677
|
+
.tlpage .counters { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
1678
|
+
.tlpage .counter { display: inline-flex; align-items: baseline; gap: 7px; padding: 7px 12px; border-radius: var(--radius-lg); background: var(--secondary); border: 1px solid var(--rule); font-size: 12.5px; color: var(--muted-foreground); }
|
|
1679
|
+
.tlpage .counter b { font-family: var(--font-heading); font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--foreground); }
|
|
1680
|
+
.tlpage .counter.run b { color: var(--primary); } .tlpage .counter.ci b { color: var(--info); } .tlpage .counter.merge b { color: var(--success); }
|
|
1681
|
+
.tlpage .counter.dep b { color: var(--warning); } .tlpage .counter.stalled b, .tlpage .counter.blocked b { color: var(--destructive); }
|
|
1682
|
+
.tlpage .counter.zero { opacity: .45; }
|
|
1683
|
+
.tlpage .headline { display: flex; align-items: center; gap: 10px; margin-top: 14px; }
|
|
1684
|
+
.tlpage .headline .lbl { font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--muted-foreground); flex: none; }
|
|
1685
|
+
.tlpage .headline .msg { font-size: 15px; font-weight: 600; } .tlpage .headline .ic { font-size: 17px; }
|
|
1686
|
+
.tlpage .t-fail, .tlpage .t-stalled { color: var(--destructive); } .tlpage .t-blocked, .tlpage .t-dep { color: var(--warning); }
|
|
1687
|
+
.tlpage .t-merge, .tlpage .t-done { color: var(--success); } .tlpage .t-review { color: var(--info); } .tlpage .t-run { color: var(--primary); } .tlpage .t-queued { color: var(--muted-foreground); }
|
|
1688
|
+
/* \u043B\u0435\u0433\u0435\u043D\u0434\u0430 \u0444\u0430\u0437 */
|
|
1689
|
+
.tlpage .legend { display: flex; flex-wrap: wrap; gap: 12px; margin: 0 0 8px 2px; font-size: 11px; color: var(--muted-foreground); }
|
|
1690
|
+
.tlpage .legend span { display: inline-flex; align-items: center; gap: 5px; }
|
|
1691
|
+
.tlpage .legend i { width: 11px; height: 8px; border-radius: 2px; display: inline-block; }
|
|
1692
|
+
.tlpage .ph-queued { background: var(--c-queued); } .tlpage .ph-running { background: var(--c-run); } .tlpage .ph-blocked { background: var(--c-blocked); }
|
|
1693
|
+
.tlpage .ph-review { background: var(--c-review); } .tlpage .ph-done { background: var(--c-done); } .tlpage .ph-failed, .tlpage .ph-stalled { background: var(--c-fail); }
|
|
1694
|
+
/* \u0442\u0430\u0439\u043C\u043B\u0430\u0439\u043D */
|
|
1695
|
+
.tlpage .tl { position: relative; }
|
|
1696
|
+
.tlpage .tl-svg { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 3; overflow: visible; }
|
|
1697
|
+
.tlpage .axis { display: grid; grid-template-columns: 210px 1fr; gap: 0; margin-bottom: 4px; position: sticky; top: 0; z-index: 4; background: var(--background); padding-top: 2px; }
|
|
1698
|
+
.tlpage .axis .ticks { position: relative; height: 20px; border-bottom: 1px solid var(--rule); }
|
|
1699
|
+
.tlpage .axis .tick { position: absolute; top: 0; bottom: 0; transform: translateX(-50%); font-size: 10px; color: var(--muted-foreground); font-family: var(--font-mono); white-space: nowrap; }
|
|
1700
|
+
.tlpage .axis .tick::after { content: ""; position: absolute; left: 50%; top: 15px; width: 1px; height: 5px; background: var(--rule); }
|
|
1701
|
+
.tlpage .lane { display: grid; grid-template-columns: 210px 1fr; align-items: stretch; border-bottom: 1px solid var(--card-border); }
|
|
1702
|
+
.tlpage .lane.dim { opacity: .58; }
|
|
1703
|
+
.tlpage .lane:hover { background: color-mix(in oklch, var(--card) 45%, transparent); }
|
|
1704
|
+
.tlpage .lane-label { padding: 10px 12px 10px 4px; min-width: 0; }
|
|
1705
|
+
.tlpage .ll-name { font-family: var(--font-heading); font-weight: 700; font-size: 13.5px; letter-spacing: .06em; display: flex; align-items: center; gap: 7px; min-width: 0; }
|
|
1706
|
+
.tlpage .ll-name .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1707
|
+
.tlpage .ll-name .bee { font-size: 13px; flex: none; }
|
|
1708
|
+
.tlpage .ll-meta { font-family: var(--font-mono); font-size: 11px; color: var(--muted-foreground); margin-top: 2px; display: flex; flex-wrap: wrap; gap: 2px 8px; }
|
|
1709
|
+
.tlpage .ll-repo { color: color-mix(in oklch, var(--muted-foreground) 80%, var(--info)); }
|
|
1710
|
+
.tlpage .pill { display: inline-flex; align-items: center; gap: 5px; font-size: 10px; font-weight: 700; padding: 1.5px 7px; border-radius: var(--radius-full); letter-spacing: .03em; text-transform: uppercase; flex: none; margin: 0; white-space: nowrap; }
|
|
1711
|
+
.tlpage .pill .sd { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
|
|
1712
|
+
.tlpage .pill.run { color: var(--primary); background: var(--primary-soft); } .tlpage .pill.run .sd { animation: pulse 1.8s infinite; }
|
|
1713
|
+
.tlpage .pill.review { color: var(--info); background: color-mix(in oklch, var(--info) 14%, transparent); }
|
|
1714
|
+
.tlpage .pill.blocked, .tlpage .pill.paused { color: var(--warning); background: color-mix(in oklch, var(--warning) 15%, transparent); }
|
|
1715
|
+
.tlpage .pill.queued { color: var(--muted-foreground); background: var(--secondary); }
|
|
1716
|
+
.tlpage .pill.done { color: var(--success); background: color-mix(in oklch, var(--success) 13%, transparent); }
|
|
1717
|
+
.tlpage .pill.stalled, .tlpage .pill.failed, .tlpage .pill.stopped, .tlpage .pill.superseded { color: var(--destructive); background: color-mix(in oklch, var(--destructive) 14%, transparent); }
|
|
1718
|
+
.tlpage .track { position: relative; padding: 14px 0; min-height: 58px; border-left: 1px solid var(--rule); }
|
|
1719
|
+
.tlpage .gridline { position: absolute; top: 0; bottom: 0; width: 1px; background: color-mix(in oklch, var(--rule) 55%, transparent); }
|
|
1720
|
+
.tlpage .seg { position: absolute; height: 15px; top: 12px; border-radius: 3px; min-width: 2px; }
|
|
1721
|
+
.tlpage .seg.queued { background: repeating-linear-gradient(45deg, color-mix(in oklch, var(--c-queued) 35%, transparent) 0 4px, transparent 4px 8px); border: 1px dashed color-mix(in oklch, var(--c-queued) 55%, transparent); }
|
|
1722
|
+
.tlpage .seg.running { background: var(--c-run); } .tlpage .seg.blocked { background: var(--c-blocked); }
|
|
1723
|
+
.tlpage .seg.review { background: var(--c-review); } .tlpage .seg.done { background: var(--c-done); }
|
|
1724
|
+
.tlpage .seg.failed, .tlpage .seg.stalled { background: var(--c-fail); }
|
|
1725
|
+
.tlpage .step-mark { position: absolute; top: 9px; width: 2px; height: 21px; background: color-mix(in oklch, var(--foreground) 55%, transparent); border-radius: 1px; transform: translateX(-1px); }
|
|
1726
|
+
.tlpage .step-mark::after { content: ""; position: absolute; top: -3px; left: -1.5px; width: 5px; height: 5px; border-radius: 50%; background: var(--foreground); opacity: .8; }
|
|
1727
|
+
.tlpage .now-mark { position: absolute; top: 4px; bottom: 4px; width: 2px; background: var(--primary); z-index: 2; box-shadow: 0 0 6px var(--primary); }
|
|
1728
|
+
.tlpage .now-mark::before { content: ""; position: absolute; top: -2px; left: -3px; width: 8px; height: 8px; border-radius: 50%; background: var(--primary); animation: pulse 1.8s infinite; }
|
|
1729
|
+
/* \u0447\u0438\u043F-\u0431\u043B\u043E\u043A\u0435\u0440 \u043D\u0430 \u043A\u0440\u0430\u044E \u0434\u043E\u0440\u043E\u0436\u043A\u0438 */
|
|
1730
|
+
.tlpage .edge { position: absolute; top: 33px; display: inline-flex; align-items: center; gap: 6px; max-width: min(560px, 70%); padding: 4px 10px; border-radius: var(--radius-full); font-size: 11.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border: 1px solid transparent; }
|
|
1731
|
+
.tlpage .edge .ic { flex: none; }
|
|
1732
|
+
.tlpage .edge.run { color: var(--primary); background: var(--primary-soft); border-color: color-mix(in oklch, var(--primary) 30%, transparent); }
|
|
1733
|
+
.tlpage .edge.review { color: var(--info); background: color-mix(in oklch, var(--info) 13%, transparent); border-color: color-mix(in oklch, var(--info) 30%, transparent); }
|
|
1734
|
+
.tlpage .edge.merge, .tlpage .edge.done { color: var(--success); background: color-mix(in oklch, var(--success) 13%, transparent); border-color: color-mix(in oklch, var(--success) 30%, transparent); }
|
|
1735
|
+
.tlpage .edge.dep, .tlpage .edge.blocked { color: var(--warning); background: color-mix(in oklch, var(--warning) 14%, transparent); border-color: color-mix(in oklch, var(--warning) 32%, transparent); }
|
|
1736
|
+
.tlpage .edge.stalled, .tlpage .edge.fail { color: var(--destructive); background: color-mix(in oklch, var(--destructive) 13%, transparent); border-color: color-mix(in oklch, var(--destructive) 30%, transparent); }
|
|
1737
|
+
.tlpage .edge.queued { color: var(--muted-foreground); background: var(--secondary); border-color: var(--rule); }
|
|
1738
|
+
.tlpage .edge.merge { box-shadow: 0 0 0 1px color-mix(in oklch, var(--success) 22%, transparent); }
|
|
1739
|
+
.tlpage .edge a { color: inherit; text-decoration: none; opacity: .85; } .tlpage .edge a:hover { text-decoration: underline; opacity: 1; }
|
|
1740
|
+
.tlpage .empty { text-align: center; color: var(--muted-foreground); padding: 60px 0; }
|
|
1741
|
+
.tlpage .dep-legend { font-size: 11px; color: var(--muted-foreground); margin-top: 14px; display: flex; align-items: center; gap: 7px; }
|
|
1742
|
+
.tlpage .dep-legend svg { width: 26px; height: 12px; flex: none; }
|
|
1743
|
+
@media (max-width: 767px) {
|
|
1744
|
+
.tlpage .axis, .tlpage .lane { grid-template-columns: 116px 1fr; }
|
|
1745
|
+
.tlpage .ll-meta { font-size: 10px; } .tlpage .ll-name { font-size: 12px; }
|
|
1746
|
+
.tlpage .headline .msg { font-size: 13.5px; }
|
|
1747
|
+
.tlpage .edge { max-width: 82%; font-size: 10.5px; }
|
|
1748
|
+
.tlpage .tl-svg { display: none; } /* \u0441\u0442\u0440\u0435\u043B\u043A\u0438 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439 \u043F\u0440\u044F\u0447\u0435\u043C \u043D\u0430 \u0443\u0437\u043A\u043E\u043C \u2014 \u0435\u0441\u0442\u044C \u0447\u0438\u043F \xAB\u0436\u0434\u0451\u0442 \u2026\xBB */
|
|
1749
|
+
}
|
|
1750
|
+
`;
|
|
1751
|
+
var FONTS = `<link href="https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700&family=Barlow+Semi+Condensed:wght@500;600;700&family=Barlow:wght@400;500&display=swap" rel="stylesheet">`;
|
|
1752
|
+
function pageShell(body) {
|
|
1753
|
+
return `<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>\u0420\u043E\u0439 \u2014 \u043A\u043E\u043D\u0441\u043E\u043B\u044C</title>${FONTS}<style>${CSS}</style></head><body>${body}</body></html>`;
|
|
1754
|
+
}
|
|
1755
|
+
function renderLogin() {
|
|
1756
|
+
return pageShell(`<div class="login-wrap"><div class="login-card">
|
|
1757
|
+
<div class="logo"><svg width="28" height="28" viewBox="0 0 32 32" fill="none" aria-hidden="true"><path d="M16 2l12 7v14l-12 7L4 23V9l12-7z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M16 9l6 3.5v7L16 23l-6-3.5v-7L16 9z" fill="currentColor" opacity=".2"/><circle cx="16" cy="16" r="2.3" fill="currentColor"/></svg></div>
|
|
1758
|
+
<h1>\u041F\u041E\u0414\u041D\u0418\u041C\u0418 \u0420\u041E\u0419</h1>
|
|
1759
|
+
<p>\u0412\u043E\u0439\u0434\u0438 \u2014 \u0438 swarm \u0447\u0438\u0442\u0430\u0435\u0442 \u0438 \u0434\u0432\u0438\u0433\u0430\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 Taskless \u043E\u0442 \u0442\u0432\u043E\u0435\u0433\u043E \u0438\u043C\u0435\u043D\u0438. \u0422\u043E\u0442 \u0436\u0435 OAuth-\u0432\u0445\u043E\u0434, \u0447\u0442\u043E \u0443 Claude Code.</p>
|
|
1760
|
+
<a class="btn-tl" href="/login"><span>\u0412\u043E\u0439\u0442\u0438 \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E</span>${TASKLESS_MARK}</a>
|
|
1761
|
+
<p style="margin-top:14px;font-size:12px">\u041F\u043E\u0434\u0440\u043E\u0431\u043D\u0435\u0435 \u043F\u0440\u043E Taskless</p>
|
|
1762
|
+
</div></div>`);
|
|
1763
|
+
}
|
|
1764
|
+
function renderDashboard(email, snap, taskUrlBase) {
|
|
1765
|
+
const initials = (email ?? "?").slice(0, 2).toUpperCase();
|
|
1766
|
+
const local = (email ?? "\u0432\u043E\u0448\u0451\u043B").split("@")[0];
|
|
1767
|
+
return pageShell(`<div class="shell">
|
|
1768
|
+
<aside class="sb">
|
|
1769
|
+
<div class="sb-head">
|
|
1770
|
+
<svg class="sb-glyph" viewBox="0 0 32 32" fill="none" aria-hidden="true"><path d="M16 2l12 7v14l-12 7L4 23V9l12-7z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M16 9l6 3.5v7L16 23l-6-3.5v-7L16 9z" fill="currentColor" opacity=".2"/><circle cx="16" cy="16" r="2.3" fill="currentColor"/></svg>
|
|
1771
|
+
<div class="sb-txt"><div class="sb-title">\u0420\u041E\u0419</div><div class="sb-sub" id="conn">overmind \xB7 ${esc(local)}</div></div>
|
|
1772
|
+
</div>
|
|
1773
|
+
<a class="item active" data-tab="home"><span data-ico="home"></span><span class="lbl">\u0413\u043B\u0430\u0432\u043D\u0430\u044F</span></a>
|
|
1774
|
+
<a class="item" data-tab="inbox"><span data-ico="inbox"></span><span class="lbl">\u0418\u043D\u0431\u043E\u043A\u0441</span><span class="badge warn" id="b-inbox"></span></a>
|
|
1775
|
+
<a class="item" data-tab="fleet"><span data-ico="bot"></span><span class="lbl">\u0420\u043E\u0439</span><span class="badge" id="b-fleet"></span></a>
|
|
1776
|
+
<a class="item" data-tab="timeline"><span data-ico="timeline"></span><span class="lbl">\u0422\u0430\u0439\u043C\u043B\u0430\u0439\u043D</span></a>
|
|
1777
|
+
<a class="item" data-tab="history"><span data-ico="history"></span><span class="lbl">\u0418\u0441\u0442\u043E\u0440\u0438\u044F</span></a>
|
|
1778
|
+
<div class="sb-foot"><span class="ws-dot">${esc(initials)}</span><div class="who"><b>${esc(local)}</b><a href="/logout">\u0432\u044B\u0439\u0442\u0438</a></div></div>
|
|
1779
|
+
</aside>
|
|
1780
|
+
<div class="content-outer">
|
|
1781
|
+
<div class="storm-banner" id="storm-banner" role="alert" hidden></div>
|
|
1782
|
+
<div class="content-card">
|
|
1783
|
+
<header class="card-head" id="card-head"></header>
|
|
1784
|
+
<div class="sf" id="sf"><div class="sf-scroll" id="sf-scroll"><main>
|
|
1785
|
+
<section id="tab-home"></section>
|
|
1786
|
+
<section id="tab-inbox" hidden></section>
|
|
1787
|
+
<section id="tab-fleet" hidden></section>
|
|
1788
|
+
<section id="tab-timeline" hidden></section>
|
|
1789
|
+
<section id="tab-history" hidden></section>
|
|
1790
|
+
</main></div></div></div></div>
|
|
1791
|
+
</div>
|
|
1792
|
+
<div class="overlay" id="overlay"><div class="modal" role="dialog" aria-modal="true" id="modal"></div></div>
|
|
1793
|
+
<script>
|
|
1794
|
+
var TASK_URL_BASE = ${jsonForScript(taskUrlBase ?? "")};
|
|
1795
|
+
var S = ${jsonForScript(snap)};
|
|
1796
|
+
var TAB = 'home', openId = null, byId = {};
|
|
1797
|
+
var filter = '', fleetSeg = 'active', spawnOpen = false;
|
|
1798
|
+
|
|
1799
|
+
/* \u2500\u2500 \u0444\u0438\u0440\u043C\u0435\u043D\u043D\u044B\u0435 \u0438\u043A\u043E\u043D\u043A\u0438 Taskless (\xABbroken outline\xBB) \u2500\u2500 */
|
|
1800
|
+
var ICONS = {
|
|
1801
|
+
home:[{t:"path",d:"M3 11l9-7 9 7v9a1 1 0 0 1-1 1h-5v-7h-6v7H4a1 1 0 0 1-1-1z",gaps:[[8,9],[58,9]]}],
|
|
1802
|
+
inbox:[{t:"path",d:"M5.4 5.1 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.4-6.9A2 2 0 0 0 16.8 4H7.2a2 2 0 0 0-1.8 1.1z",gaps:[[13,9],[60,9]]},{t:"path",d:"M22 12h-6l-2 3h-4l-2-3H2",gaps:[[42,14]],accent:1}],
|
|
1803
|
+
bot:[{t:"rect",x:4,y:8,w:16,h:12,rx:3,gaps:[[13,9],[62,9]]},{t:"path",d:"M12 4.5V8"},{t:"circle",cx:12,cy:3.4,r:1.1,fill:1,accent:1},{t:"circle",cx:9,cy:13,r:1.1,fill:1,accent:1},{t:"circle",cx:15,cy:13,r:1.1,fill:1,accent:1},{t:"path",d:"M9.5 16.5h5"}],
|
|
1804
|
+
history:[{t:"path",d:"M4 12a8 8 0 1 1 2.4 5.7",gaps:[[16,10]]},{t:"path",d:"M4 8v4h4",accent:1},{t:"path",d:"M12 8v4.5l3 1.8",accent:1}],
|
|
1805
|
+
timeline:[{t:"path",d:"M4 6.5h8"},{t:"path",d:"M4 12h13",gaps:[[62,12]]},{t:"path",d:"M4 17.5h6"},{t:"path",d:"M17.5 4.5v15",accent:1}],
|
|
1806
|
+
x:[{t:"path",d:"m18 6-12 12",gaps:[[40,20]]},{t:"path",d:"m6 6 12 12",gaps:[[40,20]]}],
|
|
1807
|
+
check:[{t:"path",d:"M4 12l5 5 11-11",gaps:[[42,14]]}]
|
|
1808
|
+
};
|
|
1809
|
+
function dash(g){ if(!g)return null; var s=[],p=0; g.slice().sort(function(a,b){return a[0]-b[0]}).forEach(function(x){s.push(Math.max(x[0]-p,0));s.push(x[1]);p=x[0]+x[1]}); s.push(Math.max(100-p,0)); return s.join(" "); }
|
|
1810
|
+
function tk(name, sw){ var ps=ICONS[name]; if(!ps)return ""; var b=ps.map(function(p){ var a=p.accent?(p.fill?' class="accf"':' class="acc"'):""; var d=p.gaps?' pathLength="100" stroke-dasharray="'+dash(p.gaps)+'"':"";
|
|
1811
|
+
if(p.t==="path")return '<path d="'+p.d+'"'+(p.fill?' fill="currentColor" stroke="none"':"")+d+a+"/>";
|
|
1812
|
+
if(p.t==="circle")return '<circle cx="'+p.cx+'" cy="'+p.cy+'" r="'+p.r+'"'+(p.fill?' fill="currentColor" stroke="none"':d)+a+"/>";
|
|
1813
|
+
if(p.t==="rect")return '<rect x="'+p.x+'" y="'+p.y+'" width="'+p.w+'" height="'+p.h+'" rx="'+(p.rx||0)+'"'+d+a+"/>"; return ""; }).join("");
|
|
1814
|
+
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="'+(sw||2.4)+'" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'+b+"</svg>"; }
|
|
1815
|
+
document.querySelectorAll("[data-ico]").forEach(function(el){ el.outerHTML = tk(el.dataset.ico); });
|
|
1816
|
+
|
|
1817
|
+
/* \u2500\u2500 \u0430\u0432\u0430\u0442\u0430\u0440\u044B-\u0433\u043B\u0438\u0444\u044B \u0434\u0440\u043E\u043D\u043E\u0432 \u2500\u2500 */
|
|
1818
|
+
var GLYPH = {
|
|
1819
|
+
claw:'<path d="M6 20c0-6 3-10 6-14 3 4 6 8 6 14M9 14h6" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/>',
|
|
1820
|
+
spore:'<circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="1.6" fill="none"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>',
|
|
1821
|
+
eye:'<path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z" stroke="currentColor" stroke-width="1.6" fill="none"/><circle cx="12" cy="12" r="2.3" fill="currentColor"/>',
|
|
1822
|
+
hex:'<path d="M12 2l8 4.6v9.2L12 21l-8-5.2V6.6L12 2z" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linejoin="round"/><circle cx="12" cy="12" r="2" fill="currentColor"/>'
|
|
1823
|
+
};
|
|
1824
|
+
var GK = ['claw','spore','eye','hex'];
|
|
1825
|
+
var ST = { running:['run','\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442','--primary'], review:['review','\u043D\u0430 \u0440\u0435\u0432\u044C\u044E','--info'], blocked:['blocked','\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430','--warning'], paused:['blocked','\u0437\u0430\u043C\u0435\u0440 \u2014 \u0436\u0434\u0451\u0442 overmind','--warning'], storm_dead:['blocked','\u0432 \u043A\u043E\u043A\u043E\u043D\u0435 (\u0448\u0442\u043E\u0440\u043C)','--warning'], done:['done','\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E','--success'], stalled:['stalled','\u043F\u043E\u0433\u0438\u0431/\u0437\u0430\u0432\u0438\u0441','--destructive'], failed:['failed','\u0443\u043F\u0430\u043B','--destructive'], stopped:['stopped','\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D','--muted-foreground'], queued:['queued','\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438','--muted-foreground'], superseded:['superseded','\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D','--muted-foreground'] };
|
|
1826
|
+
var ORDER = { running:0, blocked:1, paused:2, storm_dead:3, review:4, stalled:5, queued:6, stopped:7, done:8, failed:9, superseded:10 };
|
|
1827
|
+
var TERM = { done:1, stopped:1, failed:1, superseded:1 };
|
|
1828
|
+
var EVL = { spawned:'\u0434\u0440\u043E\u043D \u0437\u0430\u043F\u0443\u0449\u0435\u043D', step:'', blocked:'\u0432\u043E\u043F\u0440\u043E\u0441 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443', answered:'\u043F\u043E\u043B\u0443\u0447\u0435\u043D \u043E\u0442\u0432\u0435\u0442', pr_opened:'PR \u043E\u0442\u043A\u0440\u044B\u0442', merged:'PR \u0441\u043C\u0435\u0440\u0436\u0451\u043D, \u0437\u0430\u0434\u0430\u0447\u0430 \u0437\u0430\u043A\u0440\u044B\u0442\u0430', done:'\u0433\u043E\u0442\u043E\u0432\u043E', failed:'\u0434\u0440\u043E\u043D \u0443\u043F\u0430\u043B', stalled:'\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441/\u043F\u043E\u0433\u0438\u0431', respawned:'\u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435', paused:'\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D', resumed:'overmind \u0432\u0435\u0440\u043D\u0443\u043B\u0441\u044F \u2014 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B', rescued:'worktree \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u2014 \u0437\u0430\u0431\u0435\u0440\u0438 \u0440\u0443\u043A\u0430\u043C\u0438', conflict:'\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u0441 main (merge-main-in)', died:'\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440', storm_start:'\u26A1 \u043F\u0441\u0438\u043E\u043D\u043D\u0430\u044F \u0441\u0432\u044F\u0437\u044C \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430 \u2014 Anthropic \u0448\u0442\u043E\u0440\u043C\u0438\u0442', storm_end:'\u0441\u0432\u044F\u0437\u044C \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u2014 \u0440\u043E\u0439 \u043F\u0440\u043E\u0431\u0443\u0436\u0434\u0430\u0435\u0442\u0441\u044F', storm_dead:'\u0434\u0440\u043E\u043D \u0432 \u043A\u043E\u043A\u043E\u043D\u0435 (\u0448\u0442\u043E\u0440\u043C)', stopped:'\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D', superseded:'\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D' };
|
|
1829
|
+
var MDOT = { merged:'--success', done:'--success', pr_opened:'--info', spawned:'--muted-foreground', stalled:'--destructive', failed:'--destructive', respawned:'--warning', paused:'--warning', resumed:'--success', rescued:'--destructive', conflict:'--warning', died:'--destructive', storm_start:'--destructive', storm_end:'--success', storm_dead:'--warning', stopped:'--muted-foreground', superseded:'--muted-foreground' };
|
|
1830
|
+
|
|
1831
|
+
function E(s){ return String(s==null?"":s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c]}); }
|
|
1832
|
+
function color(st){ return 'var('+(ST[st]||ST.queued)[2]+')'; }
|
|
1833
|
+
function base(p){ var a=String(p||'').split('/'); while(a.length && !a[a.length-1]) a.pop(); return a[a.length-1] || String(p||''); }
|
|
1834
|
+
function plural(n,f){ var m=n%100,d=n%10; return f[(m>=11&&m<=14)?2:(d===1?0:(d>=2&&d<=4?1:2))]; }
|
|
1835
|
+
/* \u0412\u0420\u0415\u041C\u042F. \u0421\u0442\u0440\u043E\u043A\u0438 \u0438\u0437 SQLite (YYYY-MM-DD HH:MM:SS) \u2014 UTC \u0431\u0435\u0437 \u0437\u043E\u043D\u044B: \u043F\u0430\u0440\u0441\u0438\u043C \u0441\u0442\u0440\u043E\u0433\u043E \u043A\u0430\u043A
|
|
1836
|
+
UTC (\u2026+'Z'), \u0438\u043D\u0430\u0447\u0435 \u043C\u0435\u0442\u043A\u0438 \u0443\u0435\u0437\u0436\u0430\u044E\u0442 \u043D\u0430 \u0441\u043C\u0435\u0449\u0435\u043D\u0438\u0435 \u0447\u0430\u0441\u043E\u0432\u043E\u0433\u043E \u043F\u043E\u044F\u0441\u0430 \u0437\u0440\u0438\u0442\u0435\u043B\u044F. \u041C\u0435\u0442\u043A\u0438 \u0432 UI \u2014
|
|
1837
|
+
\u041E\u0422\u041D\u041E\u0421\u0418\u0422\u0415\u041B\u042C\u041D\u042B\u0415 \u043E\u0442 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0445 \u0447\u0430\u0441\u043E\u0432; \u0442\u043E\u0447\u043D\u043E\u0435 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u0435 HH:MM \u2014 \u0432 \u043C\u043E\u0434\u0430\u043B\u043A\u0435 \u0448\u0430\u0433\u043E\u0432 \u0438 title. */
|
|
1838
|
+
function pUTC(s){ if(s==null||s==='') return null; var iso=String(s).trim().replace(' ','T'); if(!/[zZ]$/.test(iso) && iso.indexOf('+')<0) iso+='Z'; var d=new Date(iso); return isNaN(d.getTime())?null:d; }
|
|
1839
|
+
function localHM(s){ var d=pUTC(s); return d ? d.toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit'}) : ''; }
|
|
1840
|
+
function localFull(s){ var d=pUTC(s); return d ? d.toLocaleString('ru-RU',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) : ''; }
|
|
1841
|
+
function rel(s){ var d=pUTC(s); if(!d) return ''; var sec=Math.max(0,(Date.now()-d.getTime())/1000);
|
|
1842
|
+
if(sec<45) return '\u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0442\u043E';
|
|
1843
|
+
var min=Math.floor(sec/60); if(min<60) return min+' \u043C\u0438\u043D \u043D\u0430\u0437\u0430\u0434';
|
|
1844
|
+
var hr=Math.floor(sec/3600); if(hr<24) return hr+' \u0447 \u043D\u0430\u0437\u0430\u0434';
|
|
1845
|
+
var now=new Date(); var days=Math.round((new Date(now.getFullYear(),now.getMonth(),now.getDate())-new Date(d.getFullYear(),d.getMonth(),d.getDate()))/86400000);
|
|
1846
|
+
if(days<=1) return '\u0432\u0447\u0435\u0440\u0430'; if(days<7) return days+' \u0434\u043D \u043D\u0430\u0437\u0430\u0434';
|
|
1847
|
+
return ('0'+d.getDate()).slice(-2)+'.'+('0'+(d.getMonth()+1)).slice(-2); }
|
|
1848
|
+
function prNum(url){ if(!url) return ''; var s=String(url); var i=s.indexOf('/pull/'); if(i>=0){ var t=s.slice(i+6).split(/[^0-9]/)[0]; if(t) return t; }
|
|
1849
|
+
var parts=s.split('/').filter(Boolean); var last=parts[parts.length-1]||''; return /^[0-9]+$/.test(last)?last:''; }
|
|
1850
|
+
function pill(st){ var s=ST[st]||ST.queued; return '<span class="pill '+s[0]+'"><span class="sd"></span>'+s[1]+'</span>'; }
|
|
1851
|
+
function ava(d){ return '<span class="ava" style="color:'+color(d.status)+'"><svg viewBox="0 0 24 24" fill="none">'+GLYPH[GK[d.id%GK.length]]+'</svg></span>'; }
|
|
1852
|
+
function taskUrl(d){ return d.taskUrl || (TASK_URL_BASE ? TASK_URL_BASE + '/tasks/' + d.slug : null); }
|
|
1853
|
+
|
|
1854
|
+
/* \u043A\u0430\u0440\u0442\u043E\u0447\u043D\u044B\u0439 \xAB\u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u0448\u0430\u0433\xBB \u043F\u043E \u0441\u0442\u0430\u0442\u0443\u0441\u0443 (\u0441\u043F\u0435\u043A\u0430 TS-3) */
|
|
1855
|
+
function cardStep(d){
|
|
1856
|
+
if(d.status==='blocked') return '\u0432\u043E\u043F\u0440\u043E\u0441 \u0432 \u0438\u043D\u0431\u043E\u043A\u0441\u0435';
|
|
1857
|
+
if(d.status==='stalled') return d.reason || '\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441/\u043F\u043E\u0433\u0438\u0431';
|
|
1858
|
+
if(d.status==='stopped') return d.stopReason || d.reason || '\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D';
|
|
1859
|
+
if(d.status==='storm_dead') return '\u0434\u0440\u043E\u043D \u0432 \u043A\u043E\u043A\u043E\u043D\u0435 \u2014 \u043F\u0441\u0438\u043E\u043D\u043D\u0430\u044F \u0441\u0432\u044F\u0437\u044C \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430 (Anthropic \u0448\u0442\u043E\u0440\u043C\u0438\u0442), worktree \u0446\u0435\u043B, \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u043E\u0442\u0431\u043E\u044F';
|
|
1860
|
+
if(d.status==='paused') return '\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430';
|
|
1861
|
+
if(d.status==='review') { var n=prNum(d.pr); return 'PR'+(n?(' #'+n):'')+' \u043D\u0430 \u0440\u0435\u0432\u044C\u044E'; }
|
|
1862
|
+
if(d.status==='done') return '\u0441\u043C\u0435\u0440\u0436\u0435\u043D';
|
|
1863
|
+
if(d.status==='queued') return (d.waiting&&d.waiting.length) ? '\u0436\u0434\u0451\u0442: '+d.waiting.join(', ') : '\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u0436\u0434\u0451\u0442 \u0441\u043B\u043E\u0442\u0430';
|
|
1864
|
+
return d.step || '\u2026';
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
/* \u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0435 \u0441\u0442\u0430\u0442\u0443\u0441\u044B (\u0441\u0435\u0433\u043C\u0435\u043D\u0442 \xAB\u0410\u043A\u0442\u0438\u0432\u043D\u044B\u0435\xBB); \u043F\u0440\u043E\u0447\u0438\u0435 \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u043D\u044B\u0435 \u2192 \xAB\u0417\u0430\u0432\u0435\u0440\u0448\u0451\u043D\u043D\u044B\u0435\xBB.
|
|
1868
|
+
paused/storm_dead \u2014 \u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0435: \u0434\u0440\u043E\u043D \u0437\u0430\u043C\u0435\u0440 \u0438 \u0416\u0414\u0401\u0422 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430, \u044D\u0442\u043E \u043D\u0435 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B (TASWM-41/23) */
|
|
1869
|
+
var ACT = { running:1, blocked:1, paused:1, storm_dead:1, review:1, queued:1 };
|
|
1870
|
+
var DRONEW = ['\u0434\u0440\u043E\u043D','\u0434\u0440\u043E\u043D\u0430','\u0434\u0440\u043E\u043D\u043E\u0432'];
|
|
1871
|
+
function isAct(d){ return !!ACT[d.status]; }
|
|
1872
|
+
|
|
1873
|
+
/* \u2500\u2500 sticky-\u0448\u0430\u043F\u043A\u0430 contentCard: \u0438\u043A\u043E\u043D\u043A\u0430 \u0440\u0430\u0437\u0434\u0435\u043B\u0430 + \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A \u0441\u043B\u0435\u0432\u0430, \u043A\u043D\u043E\u043F\u043A\u0438 \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u0441\u043F\u0440\u0430\u0432\u0430 \u2500\u2500 */
|
|
1874
|
+
function setHead(o){
|
|
1875
|
+
var actions = o.actions ? '<div class="ch-actions">'+o.actions+'</div>' : '';
|
|
1876
|
+
var tools = o.tools ? '<div class="ch-tools">'+o.tools+'</div>' : '';
|
|
1877
|
+
document.getElementById('card-head').innerHTML =
|
|
1878
|
+
'<div class="ch-row"><span class="ch-ico">'+tk(o.ico)+'</span>'+
|
|
1879
|
+
'<div class="ch-tt"><h1>'+o.title+'</h1>'+(o.sub?'<span class="sub">'+o.sub+'</span>':'')+'</div>'+actions+'</div>'+tools;
|
|
1880
|
+
}
|
|
1881
|
+
function needBtn(){ return S.questions.length ? '<button class="need" onclick="setTab(\\'inbox\\')">\u043D\u0443\u0436\u0435\u043D \u043E\u0442\u0432\u0435\u0442: '+S.questions.length+'</button>' : ''; }
|
|
1882
|
+
function droneCard(d){
|
|
1883
|
+
return '<button class="drone" data-id="'+d.id+'"><span class="d-top">'+ava(d)+
|
|
1884
|
+
'<span><span class="d-name">'+E(d.name)+'</span><span class="d-ids"><span>'+E(d.slug)+'</span><span>\xB7</span><span>'+E(base(d.repo))+'</span></span></span>'+pill(d.status)+'</span>'+
|
|
1885
|
+
'<span class="d-title">'+E(d.title || d.slug)+'</span>'+
|
|
1886
|
+
'<span class="d-step"><span class="c" style="color:'+color(d.status)+'">\u25B8</span><span class="txt">'+E(cardStep(d))+'</span></span>'+
|
|
1887
|
+
(d.rescued?'<span class="d-rescue" title="worktree \u0441 \u043D\u0435\u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u043E\u0439 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u2014 \u0437\u0430\u0431\u0435\u0440\u0438 \u0440\u0443\u043A\u0430\u043C\u0438, \u0430\u0432\u0442\u043E\u0443\u0431\u043E\u0440\u043A\u0438 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442">\u26A0 \u0435\u0441\u0442\u044C \u043D\u0435\u0441\u043F\u0430\u0441\u0451\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430</span>':'')+
|
|
1888
|
+
'<span class="d-foot"><span class="repo-tag">'+E(d.slug)+'</span><span class="time" title="'+E(localFull(d.updated))+'">'+E(rel(d.updated))+'</span></span></button>';
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/* \u2500\u2500 \u0413\u043B\u0430\u0432\u043D\u0430\u044F \u2500\u2500 */
|
|
1892
|
+
function renderHome(){
|
|
1893
|
+
var m=S.metrics;
|
|
1894
|
+
var act = S.runs.filter(function(d){ return d.status==='running'||d.status==='blocked'; });
|
|
1895
|
+
var byRepo={}; S.runs.forEach(function(d){ byRepo[d.repo]=(byRepo[d.repo]||0)+1; });
|
|
1896
|
+
var tags = Object.keys(byRepo).map(function(r){ return '<span class="repo-tag">'+E(base(r))+' \xB7 '+byRepo[r]+'</span>'; }).join('');
|
|
1897
|
+
var feed = act.length ? act.slice(0,8).map(function(d){
|
|
1898
|
+
return '<div class="feed-row"><span class="t mono" title="'+E(localFull(d.updated))+'">'+E(rel(d.updated))+'</span><span class="who">'+E(d.name)+'</span><span class="what">'+E(cardStep(d))+'</span></div>'; }).join('') : '<div class="empty">\u041D\u0435\u0442 \u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0445 \u0434\u0440\u043E\u043D\u043E\u0432.</div>';
|
|
1899
|
+
var miles = S.history.filter(function(h){ return h.kind==='milestone'; }).slice(0,6).map(function(h){
|
|
1900
|
+
return '<div class="feed-row"><span class="t mono" title="'+E(localFull(h.at))+'">'+E(rel(h.at))+'</span><span class="who" style="color:var('+(MDOT[h.event]||'--muted-foreground')+')">'+E((h.event||'').toUpperCase())+'</span><span class="what">'+E(h.name)+' \xB7 '+E(h.slug)+(h.text?' \xB7 '+E(h.text):'')+'</span></div>'; }).join('') || '<div class="empty">\u041F\u043E\u043A\u0430 \u043F\u0443\u0441\u0442\u043E.</div>';
|
|
1901
|
+
setHead({ ico:'home', title:'<span class="live-dot"></span>\u0413\u043B\u0430\u0432\u043D\u0430\u044F', sub:'\u0440\u0435\u0430\u043B\u0442\u0430\u0439\u043C \xB7 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u043F\u043E SSE', actions: needBtn() });
|
|
1902
|
+
document.getElementById('tab-home').innerHTML =
|
|
1903
|
+
'<div class="stats">'+
|
|
1904
|
+
'<div class="card stat hot"><div class="k">\u0410\u043A\u0442\u0438\u0432\u043D\u044B \u0441\u0435\u0439\u0447\u0430\u0441</div><div class="v">'+m.running+'<small>'+plural(m.running,['\u0434\u0440\u043E\u043D','\u0434\u0440\u043E\u043D\u0430','\u0434\u0440\u043E\u043D\u043E\u0432'])+'</small></div></div>'+
|
|
1905
|
+
'<div class="card stat warn"><div class="k">\u0416\u0434\u0443\u0442 \u043E\u0442\u0432\u0435\u0442\u0430</div><div class="v">'+S.questions.length+'</div></div>'+
|
|
1906
|
+
'<div class="card stat"><div class="k">\u041D\u0430 \u0440\u0435\u0432\u044C\u044E</div><div class="v">'+m.review+'<small>PR</small></div></div>'+
|
|
1907
|
+
'<div class="card stat"><div class="k">\u0421\u043C\u0435\u0440\u0436\u0435\u043D\u043E \u0441\u0435\u0433\u043E\u0434\u043D\u044F</div><div class="v">'+m.mergedToday+'<small>PR</small></div></div>'+
|
|
1908
|
+
'</div>'+
|
|
1909
|
+
'<div class="sec-h"><h2>\u0421\u0435\u0439\u0447\u0430\u0441 \u0432 \u0440\u0430\u0431\u043E\u0442\u0435</h2><span class="rule-ln"></span>'+tags+'</div><div class="card feed">'+feed+'</div>'+
|
|
1910
|
+
'<div class="sec-h"><h2>\u041F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 \u0432\u0435\u0445\u0438</h2><span class="rule-ln"></span></div><div class="card feed">'+miles+'</div>';
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
/* \u2500\u2500 \u0420\u043E\u0439: \u0434\u0432\u0430 \u0441\u0435\u0433\u043C\u0435\u043D\u0442\u0430 \u2014 \xAB\u0410\u043A\u0442\u0438\u0432\u043D\u044B\u0435\xBB (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E) \u0438 \xAB\u0417\u0430\u0432\u0435\u0440\u0448\u0451\u043D\u043D\u044B\u0435\xBB \u2500\u2500 */
|
|
1914
|
+
function setFilter(k){ filter=k; renderFleet(); }
|
|
1915
|
+
function setFleetSeg(s){ fleetSeg=s; filter=''; document.getElementById('sf-scroll').scrollTop=0; renderFleet(); sfUpdate(); }
|
|
1916
|
+
function toggleSpawn(){ spawnOpen=!spawnOpen; renderFleet(); if(spawnOpen){ var i=document.querySelector('#tab-fleet .spawnpanel input[name="slug"]'); if(i) i.focus(); } }
|
|
1917
|
+
function allRepos(){ var r=[]; S.runs.forEach(function(d){ if(r.indexOf(d.repo)<0) r.push(d.repo); }); return r; }
|
|
1918
|
+
function fleetSection(title, tag, ds){
|
|
1919
|
+
return (title?'<div class="sec-h"><h2>'+title+'</h2><span class="rule-ln"></span>'+(tag||'')+'</div>':'')+
|
|
1920
|
+
'<div class="grid">'+ds.map(droneCard).join('')+'</div>';
|
|
1921
|
+
}
|
|
1922
|
+
var orderSort = function(a,b){ return (ORDER[a.status]-ORDER[b.status]) || b.id-a.id; };
|
|
1923
|
+
var updSort = function(a,b){ return a.updated<b.updated?1:(a.updated>b.updated?-1:b.id-a.id); };
|
|
1924
|
+
function renderFleet(){
|
|
1925
|
+
var active = S.runs.filter(isAct), completed = S.runs.filter(function(d){ return !isAct(d); });
|
|
1926
|
+
var list = fleetSeg==='active' ? active : completed;
|
|
1927
|
+
// \u0440\u0435\u043F\u043E-\u0444\u0438\u043B\u044C\u0442\u0440 \u043F\u043E \u0442\u0435\u043A\u0443\u0449\u0435\u043C\u0443 \u0441\u0435\u0433\u043C\u0435\u043D\u0442\u0443; single-repo (\u0442\u0438\u043F\u0438\u0447\u043D\u044B\u0439 \u0440\u043E\u0439) \u2014 \u0431\u0435\u0437 \u043F\u0438\u043B\u044E\u043B\u044C
|
|
1928
|
+
var repos=[]; list.forEach(function(d){ if(repos.indexOf(d.repo)<0) repos.push(d.repo); });
|
|
1929
|
+
var multi = repos.length>1;
|
|
1930
|
+
if(filter && repos.indexOf(filter)<0) filter='';
|
|
1931
|
+
|
|
1932
|
+
var seg = '<div class="seg">'+
|
|
1933
|
+
'<button class="'+(fleetSeg==='active'?'on':'')+'" onclick="setFleetSeg(\\'active\\')">\u0410\u043A\u0442\u0438\u0432\u043D\u044B\u0435 <span class="n">'+active.length+'</span></button>'+
|
|
1934
|
+
'<button class="'+(fleetSeg==='done'?'on':'')+'" onclick="setFleetSeg(\\'done\\')">\u0417\u0430\u0432\u0435\u0440\u0448\u0451\u043D\u043D\u044B\u0435 <span class="n">'+completed.length+'</span></button>'+
|
|
1935
|
+
'</div>';
|
|
1936
|
+
var spawnBtn = '<button class="ch-btn'+(spawnOpen?' on':'')+'" onclick="toggleSpawn()">'+(spawnOpen?'\xD7 \u0421\u0432\u0435\u0440\u043D\u0443\u0442\u044C':'+ \u041F\u043E\u0434\u043D\u044F\u0442\u044C \u0434\u0440\u043E\u043D\u0430')+'</button>';
|
|
1937
|
+
var sub = fleetSeg==='active'
|
|
1938
|
+
? '<b>'+active.length+'</b> '+plural(active.length,['\u0430\u043A\u0442\u0438\u0432\u0435\u043D','\u0430\u043A\u0442\u0438\u0432\u043D\u044B','\u0430\u043A\u0442\u0438\u0432\u043D\u044B'])+' \xB7 '+S.metrics.running+' \u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442'
|
|
1939
|
+
: '<b>'+completed.length+'</b> \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E \u0437\u0430 \u0441\u0443\u0442\u043A\u0438';
|
|
1940
|
+
setHead({ ico:'bot', title:'\u0420\u043E\u0439', sub:sub, actions: needBtn()+spawnBtn, tools: seg });
|
|
1941
|
+
|
|
1942
|
+
var html='';
|
|
1943
|
+
if(spawnOpen){
|
|
1944
|
+
var rs = allRepos();
|
|
1945
|
+
html += '<form class="spawnpanel" data-ajax action="/spawn"><input class="mono" name="slug" placeholder="SLUG" style="flex:0 0 100px" required>'+
|
|
1946
|
+
'<input name="area" placeholder="area (\u043D\u0430\u043F\u0440. triage-ui)" style="flex:1 1 140px">'+
|
|
1947
|
+
'<input class="mono" name="repo" list="repos" placeholder="repo (\u043F\u0443\u0441\u0442\u043E \u2192 \u0434\u0435\u0444\u043E\u043B\u0442)" style="flex:1 1 150px">'+
|
|
1948
|
+
'<button class="btn" type="submit">+ \u0432 \u0440\u043E\u0439</button>'+
|
|
1949
|
+
'<datalist id="repos">'+rs.map(function(k){return '<option value="'+E(k)+'">';}).join('')+'</datalist></form>';
|
|
1950
|
+
}
|
|
1951
|
+
if(multi){
|
|
1952
|
+
html += '<div class="repofilter"><button class="rf'+(filter===''?' on':'')+'" onclick="setFilter(\\'\\')">\u0432\u0441\u0435</button>';
|
|
1953
|
+
repos.forEach(function(k){ html += '<button class="rf'+(filter===k?' on':'')+'" data-k="'+E(k)+'" onclick="setFilter(this.dataset.k)">'+E(base(k))+'</button>'; });
|
|
1954
|
+
html += '</div>';
|
|
1955
|
+
}
|
|
1956
|
+
var view = list.filter(function(d){ return !(multi && filter) || d.repo===filter; });
|
|
1957
|
+
if(!view.length){
|
|
1958
|
+
html += '<div class="empty">'+(fleetSeg==='active' ? '\u0410\u043A\u0442\u0438\u0432\u043D\u044B\u0445 \u0434\u0440\u043E\u043D\u043E\u0432 \u043D\u0435\u0442 \u2014 \u0440\u043E\u0439 \u043E\u0442\u0434\u044B\u0445\u0430\u0435\u0442. \u041F\u043E\u0434\u043D\u0438\u043C\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0432\u044B\u0448\u0435.' : '\u0417\u0430 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 \u0441\u0443\u0442\u043A\u0438 \u043D\u0438\u043A\u0442\u043E \u043D\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B \u0438 \u043D\u0435 \u0443\u043F\u0430\u043B.')+'</div>';
|
|
1959
|
+
} else if(fleetSeg==='active'){
|
|
1960
|
+
if(multi && !filter){
|
|
1961
|
+
repos.forEach(function(k){
|
|
1962
|
+
var ds = active.filter(function(d){ return d.repo===k; }).sort(orderSort);
|
|
1963
|
+
html += fleetSection(E(base(k)||'\u2014'), '<span class="repo-tag">'+ds.length+' '+plural(ds.length,DRONEW)+'</span>', ds);
|
|
1964
|
+
});
|
|
1965
|
+
} else {
|
|
1966
|
+
html += fleetSection('', '', view.slice().sort(orderSort));
|
|
1967
|
+
}
|
|
1968
|
+
} else {
|
|
1969
|
+
// \xAB\u0417\u0430\u0432\u0435\u0440\u0448\u0451\u043D\u043D\u044B\u0435\xBB \u2192 \u0441\u0434\u0435\u043B\u0430\u043B\u0438 \u2713 / \u043F\u0440\u0435\u0440\u0432\u0430\u043D\u044B \u26A0
|
|
1970
|
+
var did = view.filter(function(d){ return d.status==='done'; }).sort(updSort);
|
|
1971
|
+
var broke = view.filter(function(d){ return d.status!=='done'; }).sort(updSort);
|
|
1972
|
+
if(did.length) html += fleetSection('\u0421\u0434\u0435\u043B\u0430\u043B\u0438 \u2713', '<span class="repo-tag">'+did.length+' '+plural(did.length,DRONEW)+'</span>', did);
|
|
1973
|
+
if(broke.length) html += fleetSection('\u041F\u0440\u0435\u0440\u0432\u0430\u043D\u044B \u26A0', '<span class="repo-tag">'+broke.length+' '+plural(broke.length,DRONEW)+'</span>', broke);
|
|
1974
|
+
}
|
|
1975
|
+
document.getElementById('tab-fleet').innerHTML = html;
|
|
1976
|
+
document.querySelectorAll('#tab-fleet .drone').forEach(function(b){ b.addEventListener('click', function(){ openModal(+b.dataset.id); }); });
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
/* \u2500\u2500 \u0418\u043D\u0431\u043E\u043A\u0441 \u2500\u2500 */
|
|
1980
|
+
function renderInbox(){
|
|
1981
|
+
var q = S.questions;
|
|
1982
|
+
var body = q.length ? q.map(function(x){
|
|
1983
|
+
var tier = x.escalated ? '<span class="tier human">\u0440\u0435\u0448\u0430\u0435\u0442 \u0447\u0435\u043B\u043E\u0432\u0435\u043A</span>' : '<span class="tier orc">\u043E\u0440\u043A\u0435\u0441\u0442\u0440\u0430\u0442\u043E\u0440</span>';
|
|
1984
|
+
var opts = (x.options||[]).map(function(o){ return '<button class="opt" data-qid="'+x.id+'" data-opt="'+E(o)+'" onclick="answerOpt(this)">'+E(o)+'</button>'; }).join('');
|
|
1985
|
+
return '<div class="card q" id="q-'+x.id+'"><div class="q-top"><div><div class="q-meta">'+E(x.name)+' \xB7 '+E(x.slug)+' \xB7 '+E(base(x.repo))+'</div><div class="q-text">'+E(x.text)+'</div></div>'+tier+'</div>'+
|
|
1986
|
+
(opts?'<div class="opts">'+opts+'</div>':'')+
|
|
1987
|
+
'<form class="answer-row" data-ajax action="/answer"><input type="hidden" name="qid" value="'+x.id+'"><input name="text" placeholder="'+(opts?'\u2026\u0438\u043B\u0438 \u0441\u0432\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 \u2014 \u0434\u0440\u043E\u043D \u043F\u043E\u043B\u0443\u0447\u0438\u0442 \u0435\u0433\u043E \u0438 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442':'\u043E\u0442\u0432\u0435\u0442 \u0434\u0440\u043E\u043D\u0443')+'"'+(opts?'':' required')+'><button class="btn" type="submit">\u041E\u0442\u0432\u0435\u0442\u0438\u0442\u044C</button></form></div>';
|
|
1988
|
+
}).join('') : '<div class="card"><div class="empty">\u0418\u043D\u0431\u043E\u043A\u0441 \u043F\u0443\u0441\u0442 \u2014 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u043A \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443 \u043D\u0435\u0442.</div></div>';
|
|
1989
|
+
setHead({ ico:'inbox', title:'\u0418\u043D\u0431\u043E\u043A\u0441', sub:'\u0432\u043E\u043F\u0440\u043E\u0441\u044B, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0436\u0434\u0443\u0442 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430' });
|
|
1990
|
+
document.getElementById('tab-inbox').innerHTML =
|
|
1991
|
+
body+
|
|
1992
|
+
'<p class="hint">\u0422\u0435\u0445\u043D\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0438 \u0434\u0440\u043E\u043D\u044B \u0440\u0435\u0448\u0430\u044E\u0442 \u0441 \u043E\u0440\u043A\u0435\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u043E\u043C \u2014 \u0441\u044E\u0434\u0430 \u043F\u043E\u043F\u0430\u0434\u0430\u0435\u0442 \u043F\u0440\u043E\u0434\u0443\u043A\u0442\u043E\u0432\u043E\u0435. \u041E\u0442\u0432\u0435\u0442\u044B \u043E\u0440\u043A\u0435\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0430 \u0432\u0438\u0434\u043D\u044B \u0432 \u0418\u0441\u0442\u043E\u0440\u0438\u0438 \u0441 cyan-\u043F\u043E\u043C\u0435\u0442\u043A\u043E\u0439.</p>';
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
/* \u2500\u2500 \u0418\u0441\u0442\u043E\u0440\u0438\u044F: \u043F\u043E\u0434\u0440\u043E\u0431\u043D\u0430\u044F \u043B\u0435\u043D\u0442\u0430 (\u0433\u043B\u0438\u0444+\u0438\u043C\u044F \xB7 \u0446\u0432\u0435\u0442\u043D\u0430\u044F \u043F\u0438\u043B\u044E\u043B\u044F \u0441\u043E\u0431\u044B\u0442\u0438\u044F \xB7 slug+\u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A \u0437\u0430\u0434\u0430\u0447\u0438
|
|
1996
|
+
\xB7 \u0441\u0443\u0442\u044C/Q&A \xB7 \u0441\u0441\u044B\u043B\u043A\u0438 PR+\u0437\u0430\u0434\u0430\u0447\u0430 \xB7 \u043E\u0442\u043D\u043E\u0441\u0438\u0442. \u0432\u0440\u0435\u043C\u044F + \u0442\u043E\u0447\u043D\u043E\u0435 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u0435 \u0432 title). \u2500\u2500 */
|
|
1997
|
+
function glyphFor(s){ s=String(s||''); var n=0; for(var i=0;i<s.length;i++) n=(n+s.charCodeAt(i))%GK.length; return GLYPH[GK[n]]; }
|
|
1998
|
+
function htimeCell(h){ return '<div class="htime" title="'+E(localFull(h.at))+'"><span class="rel">'+E(rel(h.at))+'</span><span class="abs">'+E(localHM(h.at))+'</span></div>'; }
|
|
1999
|
+
function hpill(label, cv){ return '<span class="hpill" style="color:var('+cv+');background:color-mix(in oklch, var('+cv+') 15%, transparent)">'+E(label)+'</span>'; }
|
|
2000
|
+
function hHead(h, pillHtml, extra){ return '<div class="hhead"><span class="hh-ava"><svg viewBox="0 0 24 24" fill="none">'+glyphFor(h.name)+'</svg></span><span class="hh-name">'+E(h.name)+'</span>'+pillHtml+'<span class="hh-slug">'+E(h.slug)+'</span>'+(extra||'')+'</div>'; }
|
|
2001
|
+
function hTaskUrl(h){ return h.taskUrl || (TASK_URL_BASE ? TASK_URL_BASE+'/tasks/'+h.slug : null); }
|
|
2002
|
+
function hLinks(h){ var u=hTaskUrl(h), n=prNum(h.pr), a=[];
|
|
2003
|
+
if(u) a.push('<a href="'+E(u)+'" target="_blank" rel="noopener">\u0437\u0430\u0434\u0430\u0447\u0430 '+E(h.slug)+' \u2197</a>');
|
|
2004
|
+
if(h.pr) a.push('<a href="'+E(h.pr)+'" target="_blank" rel="noopener">PR'+(n?' #'+n:'')+' \u2197</a>');
|
|
2005
|
+
return a.length ? '<div class="hlinks">'+a.join('')+'</div>' : ''; }
|
|
2006
|
+
function histQa(h){
|
|
2007
|
+
var opts = (h.options||[]).map(function(o){ var pick = h.answer===o; var click = (!pick && !h.open);
|
|
2008
|
+
return '<span class="hopt'+(pick?' pick':'')+(click?' click':'')+'"'+(click?' data-qid="'+h.qid+'" data-opt="'+E(o)+'" onclick="replayOpt(this)"':'')+'>'+E(o)+(pick?' \u2713':'')+'</span>'; }).join('');
|
|
2009
|
+
var custom='';
|
|
2010
|
+
var isCustom = h.answer!=null && !(h.options||[]).some(function(o){ return o===h.answer; });
|
|
2011
|
+
if(isCustom){ var human = h.answered_by==='human';
|
|
2012
|
+
custom = '<div class="hcustom '+(human?'human':'orc')+'"><div><div class="who">'+(human?'\u0447\u0435\u043B\u043E\u0432\u0435\u043A \xB7 \u0441\u0432\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442':'\u043E\u0440\u043A\u0435\u0441\u0442\u0440\u0430\u0442\u043E\u0440 \xB7 \u0441\u0432\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442')+'</div><div class="val">'+E(h.answer)+'</div></div></div>';
|
|
2013
|
+
} else if(h.open){ custom = '<div class="hpending">\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430\u2026</div>'; }
|
|
2014
|
+
var cv = h.escalated ? '--warning' : '--info';
|
|
2015
|
+
var extra = h.escalated ? '<span class="hh-slug">\xB7 \u0431\u044B\u043B \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A</span>' : '';
|
|
2016
|
+
return '<div class="hrow">'+htimeCell(h)+'<div class="hbody">'+hHead(h, hpill(h.open?'\u0432\u043E\u043F\u0440\u043E\u0441':'\u043E\u0442\u0432\u0435\u0442', cv), extra)+
|
|
2017
|
+
(h.title?'<div class="htitle">'+E(h.title)+'</div>':'')+
|
|
2018
|
+
'<div class="hq">'+E(h.question)+'</div>'+(opts?'<div class="hopts">'+opts+'</div>':'')+custom+hLinks(h)+'</div></div>';
|
|
2019
|
+
}
|
|
2020
|
+
function histMile(h){ var c=MDOT[h.event]||'--muted-foreground'; var lbl=EVL[h.event]||h.event;
|
|
2021
|
+
return '<div class="hrow">'+htimeCell(h)+'<div class="hbody">'+hHead(h, hpill((h.event||'').toUpperCase(), c))+
|
|
2022
|
+
(h.title?'<div class="htitle">'+E(h.title)+'</div>':'')+
|
|
2023
|
+
'<div class="hessence">'+E(h.text||lbl)+'</div>'+hLinks(h)+'</div></div>'; }
|
|
2024
|
+
function renderHistory(){
|
|
2025
|
+
var rows = S.history.map(function(h){ return h.kind==='milestone' ? histMile(h) : histQa(h); }).join('');
|
|
2026
|
+
setHead({ ico:'history', title:'\u0418\u0441\u0442\u043E\u0440\u0438\u044F', sub:'\u0440\u0435\u0448\u0435\u043D\u0438\u044F \u0438 \u0432\u0435\u0445\u0438 \u2014 \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u0443\u0434\u0430\u043B\u044F\u0435\u0442\u0441\u044F' });
|
|
2027
|
+
document.getElementById('tab-history').innerHTML =
|
|
2028
|
+
'<div class="card">'+(rows || '<div class="empty">\u0418\u0441\u0442\u043E\u0440\u0438\u0438 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442.</div>')+'</div>';
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
/* \u2500\u2500 \u0422\u0430\u0439\u043C\u043B\u0430\u0439\u043D (\u043F\u043E\u0440\u0442 TASWM-47): \u0441\u0432\u043E\u044F \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430, \u043F\u043E\u043B\u043B\u0438\u0442 /timeline \u0440\u0430\u0437 \u0432 15\u0441 (\u0442\u044F\u0436\u0451\u043B\u044B\u0439 CI
|
|
2032
|
+
\u043D\u0435 \u0442\u0430\u0449\u0438\u043C \u0432 1-\u0441\u0435\u043A SSE). \u041A\u0430\u0440\u043A\u0430\u0441 \u0441\u0442\u0440\u043E\u0438\u0442\u0441\u044F \u043E\u0434\u0438\u043D \u0440\u0430\u0437, tlPaint \u043F\u0435\u0440\u0435\u0440\u0438\u0441\u043E\u0432\u044B\u0432\u0430\u0435\u0442 \u0434\u043E\u0440\u043E\u0436\u043A\u0438. \u2500\u2500 */
|
|
2033
|
+
var TL_SHELL =
|
|
2034
|
+
'<div class="tlpage">'+
|
|
2035
|
+
'<div class="now-head"><div class="counters" id="tl-counters"></div>'+
|
|
2036
|
+
'<div class="headline" id="tl-headline" hidden><span class="lbl">\u0433\u043B\u0430\u0432\u043D\u043E\u0435 \u0441\u0435\u0439\u0447\u0430\u0441</span><span class="ic" id="tl-hl-ic"></span><span class="msg" id="tl-hl-msg"></span></div>'+
|
|
2037
|
+
'</div>'+
|
|
2038
|
+
'<div class="legend">'+
|
|
2039
|
+
'<span><i class="ph-queued"></i>\u043E\u0447\u0435\u0440\u0435\u0434\u044C</span><span><i class="ph-running"></i>\u0440\u0430\u0431\u043E\u0442\u0430</span>'+
|
|
2040
|
+
'<span><i class="ph-blocked"></i>\u0432\u043E\u043F\u0440\u043E\u0441</span><span><i class="ph-review"></i>review</span>'+
|
|
2041
|
+
'<span><i class="ph-done"></i>\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E</span><span><i class="ph-failed"></i>stalled/\u0443\u043F\u0430\u043B</span>'+
|
|
2042
|
+
'<span style="margin-left:auto">\u25AE \u0448\u0430\u0433 \u0434\u0440\u043E\u043D\u0430 \xB7 <span style="color:var(--primary)">\u25AE</span> \u0441\u0435\u0439\u0447\u0430\u0441</span>'+
|
|
2043
|
+
'</div>'+
|
|
2044
|
+
'<div class="tl" id="tl"><div class="axis"><div></div><div class="ticks" id="tl-ticks"></div></div>'+
|
|
2045
|
+
'<div id="tl-lanes"></div><svg class="tl-svg" id="tl-edges"></svg></div>'+
|
|
2046
|
+
'<div class="dep-legend"><svg viewBox="0 0 26 12"><defs><marker id="lg-ah" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto"><path d="M0 0L6 3L0 6z" fill="var(--warning)"/></marker></defs><path d="M1 6 H20" stroke="var(--warning)" stroke-width="1.5" fill="none" marker-end="url(#lg-ah)" stroke-dasharray="3 2"/></svg>'+
|
|
2047
|
+
' \u0441\u0442\u0440\u0435\u043B\u043A\u0430 = \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C <b>spawn --after</b>: \u0434\u0440\u043E\u043D \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0436\u0434\u0451\u0442 <b>done</b> \u0434\u0440\u0443\u0433\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430</div>'+
|
|
2048
|
+
'</div>';
|
|
2049
|
+
var tlAX0=0, tlAX1=1, tlTimer=null, TL_LAST=null;
|
|
2050
|
+
function tlMs(iso){ return Number(new Date((iso.indexOf('Z')>=0||iso.indexOf('T')>=0)?iso:iso.replace(' ','T')+'Z')); }
|
|
2051
|
+
function tlFmtTime(iso){ var d=new Date((iso.indexOf('Z')>=0||iso.indexOf('T')>=0)?iso:iso.replace(' ','T')+'Z'); return d.toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit'}); }
|
|
2052
|
+
function tlPos(iso){ return ((tlMs(iso)-tlAX0)/(tlAX1-tlAX0))*100; }
|
|
2053
|
+
function tlFmtDur(msv){ var m=Math.round(msv/60000); if(m<60) return m+'\u043C'; var h=Math.floor(m/60); return h+'\u0447'+(m%60?' '+(m%60)+'\u043C':''); }
|
|
2054
|
+
function tlRenderAxis(start, end){
|
|
2055
|
+
tlAX0=tlMs(start); tlAX1=tlMs(end); if(tlAX1<=tlAX0) tlAX1=tlAX0+60000;
|
|
2056
|
+
var span=tlAX1-tlAX0;
|
|
2057
|
+
var steps=[15,30,60,120,180,360].map(function(m){ return m*60000; });
|
|
2058
|
+
var step=steps.find(function(x){ return span/x<=7; })||steps[steps.length-1];
|
|
2059
|
+
var ticks=document.getElementById('tl-ticks'); ticks.innerHTML='';
|
|
2060
|
+
var first=Math.ceil(tlAX0/step)*step;
|
|
2061
|
+
for(var t=first;t<=tlAX1;t+=step){ var el=document.createElement('div'); el.className='tick'; el.style.left=(((t-tlAX0)/span)*100)+'%'; el.textContent=tlFmtTime(new Date(t).toISOString()); ticks.appendChild(el); }
|
|
2062
|
+
return { step:step, span:span };
|
|
2063
|
+
}
|
|
2064
|
+
function tlRenderCounters(c){
|
|
2065
|
+
var defs=[['run','\u25B6','\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442',c.running],['ci','\u23F3','\u0436\u0434\u0451\u0442 CI',c.waitingCI],['merge','\u{1F535}','\u0436\u0434\u0451\u0442 \u043C\u0435\u0440\u0436\u0430',c.waitingMerge],['dep','\u26D4','\u0436\u0434\u0451\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C',c.waitingDep],['blocked','\u2753','\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430',c.blocked],['stalled','\u26A0\uFE0F','stalled',c.stalled]];
|
|
2066
|
+
document.getElementById('tl-counters').innerHTML=defs.map(function(d){ return '<span class="counter '+d[0]+(d[3]?'':' zero')+'">'+d[1]+' <b>'+d[3]+'</b> '+d[2]+'</span>'; }).join('');
|
|
2067
|
+
}
|
|
2068
|
+
function tlLaneHTML(l, gridTicks){
|
|
2069
|
+
var dim=(l.live && !l.obsolete)?'':' dim';
|
|
2070
|
+
var glines=gridTicks.map(function(p){ return '<div class="gridline" style="left:'+p+'%"></div>'; }).join('');
|
|
2071
|
+
var segs=l.segments.map(function(s){
|
|
2072
|
+
var left=Math.max(0,tlPos(s.from)), right=Math.min(100,tlPos(s.to));
|
|
2073
|
+
if(right<=0||left>=100) return '';
|
|
2074
|
+
var w=Math.max(0.4,right-left);
|
|
2075
|
+
return '<div class="seg '+s.phase+'" style="left:'+left+'%;width:'+w+'%" title="'+E(s.phase)+'"></div>';
|
|
2076
|
+
}).join('');
|
|
2077
|
+
var steps=l.steps.map(function(s){ var p=tlPos(s.at); return (p<0||p>100)?'':'<div class="step-mark" style="left:'+p+'%" title="'+E(s.text)+' \xB7 '+tlFmtTime(s.at)+'"></div>'; }).join('');
|
|
2078
|
+
var nowMark=l.live?'<div class="now-mark" style="left:'+Math.min(100,tlPos(l.end))+'%"></div>':'';
|
|
2079
|
+
var edgeLeft=Math.min(tlPos(l.end),99);
|
|
2080
|
+
var b=l.blocker; var extra='';
|
|
2081
|
+
if(l.pr) extra=' \xB7 <a href="'+E(l.pr)+'" target="_blank" rel="noopener">PR \u2197</a>';
|
|
2082
|
+
var edge='<div class="edge '+b.tone+'" style="left:'+edgeLeft+'%;transform:translateX('+(edgeLeft>55?'-100%':'0')+')"><span class="ic">'+b.icon+'</span><span class="etxt">'+E(b.text)+'</span>'+extra+'</div>';
|
|
2083
|
+
var pillCls=l.obsolete?'queued':(l.status==='running'?'run':l.status);
|
|
2084
|
+
var dur=tlFmtDur(tlMs(l.end)-tlMs(l.start));
|
|
2085
|
+
return '<div class="lane'+dim+'" data-id="'+l.id+'" data-slug="'+E(l.slug)+'">'+
|
|
2086
|
+
'<div class="lane-label"><div class="ll-name"><span class="bee">\u{1F41D}</span><span class="nm">'+E(l.name)+'</span>'+
|
|
2087
|
+
'<span class="pill '+pillCls+'">'+(l.status==='running'?'<span class="sd"></span>':'')+E(l.status)+'</span></div>'+
|
|
2088
|
+
'<div class="ll-meta"><span>'+E(l.slug)+'</span><span class="ll-repo">'+E(l.repo)+'</span>'+(l.area?'<span>\xB7 '+E(l.area)+'</span>':'')+'<span>\xB7 '+E(dur)+'</span></div>'+
|
|
2089
|
+
'</div><div class="track">'+glines+segs+steps+nowMark+edge+'</div></div>';
|
|
2090
|
+
}
|
|
2091
|
+
function tlDrawEdges(lanes){
|
|
2092
|
+
var svg=document.getElementById('tl-edges');
|
|
2093
|
+
svg.innerHTML='<defs><marker id="ah" markerWidth="7" markerHeight="7" refX="6" refY="3" orient="auto"><path d="M0 0L7 3L0 6z" fill="var(--warning)"/></marker></defs>';
|
|
2094
|
+
var tl=document.getElementById('tl').getBoundingClientRect();
|
|
2095
|
+
var laneEl={}; document.querySelectorAll('#tl-lanes .lane').forEach(function(el){ laneEl[el.dataset.id]=el; });
|
|
2096
|
+
for(var i=0;i<lanes.length;i++){ var l=lanes[i]; if(l.status!=='queued') continue;
|
|
2097
|
+
for(var j=0;j<l.dependsOn.length;j++){ var d=l.dependsOn[j]; if(d.met||d.runId==null) continue;
|
|
2098
|
+
var from=laneEl[l.id], to=laneEl[d.runId]; if(!from||!to) continue;
|
|
2099
|
+
var fr=from.getBoundingClientRect(), tr=to.getBoundingClientRect();
|
|
2100
|
+
var x1=fr.left-tl.left+6, y1=fr.top-tl.top+fr.height/2, x2=tr.left-tl.left+6, y2=tr.top-tl.top+tr.height/2;
|
|
2101
|
+
var bow=Math.max(24,Math.abs(y2-y1)*0.35);
|
|
2102
|
+
var path=document.createElementNS('http://www.w3.org/2000/svg','path');
|
|
2103
|
+
path.setAttribute('d','M'+x1+' '+y1+' C'+(x1-bow)+' '+y1+','+(x2-bow)+' '+y2+','+x2+' '+y2);
|
|
2104
|
+
path.setAttribute('fill','none'); path.setAttribute('stroke','var(--warning)'); path.setAttribute('stroke-width','1.6');
|
|
2105
|
+
path.setAttribute('stroke-dasharray','4 3'); path.setAttribute('marker-end','url(#ah)'); path.setAttribute('opacity','0.85');
|
|
2106
|
+
svg.appendChild(path);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
function tlPaint(s){
|
|
2111
|
+
var upd=document.getElementById('tl-upd'); if(upd) upd.textContent='\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u043E '+tlFmtTime(s.now);
|
|
2112
|
+
tlRenderCounters(s.counters);
|
|
2113
|
+
var hl=document.getElementById('tl-headline');
|
|
2114
|
+
if(s.headline){ hl.hidden=false; document.getElementById('tl-hl-ic').textContent=s.headline.icon; var m=document.getElementById('tl-hl-msg'); m.textContent=s.headline.text; m.className='msg t-'+s.headline.tone; }
|
|
2115
|
+
else hl.hidden=true;
|
|
2116
|
+
var a=tlRenderAxis(s.axisStart, s.axisEnd), step=a.step, span=a.span;
|
|
2117
|
+
var gridTicks=[], first=Math.ceil(tlAX0/step)*step;
|
|
2118
|
+
for(var t=first;t<=tlAX1;t+=step) gridTicks.push(((t-tlAX0)/span)*100);
|
|
2119
|
+
var lanesEl=document.getElementById('tl-lanes');
|
|
2120
|
+
if(!s.lanes.length){ lanesEl.innerHTML='<div class="empty">\u041D\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432 \u0432 \u043E\u043A\u043D\u0435. \u0420\u043E\u0439 \u0441\u043F\u0438\u0442 \u{1F41D}</div>'; document.getElementById('tl-edges').innerHTML=''; sfUpdate(); return; }
|
|
2121
|
+
lanesEl.innerHTML=s.lanes.map(function(l){ return tlLaneHTML(l, gridTicks); }).join('');
|
|
2122
|
+
requestAnimationFrame(function(){ tlDrawEdges(s.lanes); });
|
|
2123
|
+
sfUpdate();
|
|
2124
|
+
}
|
|
2125
|
+
function tlFetch(){ fetch('/timeline').then(function(r){ return r.json(); }).then(function(s){ TL_LAST=s; tlPaint(s); }).catch(function(){}); }
|
|
2126
|
+
function tlEnsure(){ if(tlTimer) return;
|
|
2127
|
+
setHead({ ico:'timeline', title:'\u0422\u0430\u0439\u043C\u043B\u0430\u0439\u043D', sub:'\u043A\u0442\u043E \u0447\u0435\u043C \u0437\u0430\u043D\u044F\u0442 \u0438 <b>\u0447\u0442\u043E \u043A\u043E\u0433\u043E \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442</b> \u2014 \u0441 \u043E\u0434\u043D\u043E\u0433\u043E \u0432\u0437\u0433\u043B\u044F\u0434\u0430', actions:'<span class="repo-tag"><span class="live-dot"></span><span id="tl-upd">\u043E\u0431\u043D\u043E\u0432\u043B\u044F\u044E\u2026</span></span>' });
|
|
2128
|
+
document.getElementById('tab-timeline').innerHTML=TL_SHELL;
|
|
2129
|
+
tlFetch(); tlTimer=setInterval(tlFetch, 15000);
|
|
2130
|
+
}
|
|
2131
|
+
function tlStop(){ if(tlTimer){ clearInterval(tlTimer); tlTimer=null; } }
|
|
2132
|
+
window.addEventListener('resize', function(){ if(TL_LAST && TAB==='timeline') tlDrawEdges(TL_LAST.lanes); });
|
|
2133
|
+
|
|
2134
|
+
/* \u2500\u2500 \u043F\u043E\u0434\u0441\u0432\u0435\u0442\u043A\u0430 \u0441\u0438\u043D\u0442\u0430\u043A\u0441\u0438\u0441\u0430: \u0438\u043D\u043B\u0430\u0439\u043D, \u0431\u0435\u0437 CDN. \u041B\u0451\u0433\u043A\u0438\u0439 \u043E\u0434\u043D\u043E\u043F\u0440\u043E\u0445\u043E\u0434\u043D\u044B\u0439 \u0442\u043E\u043A\u0435\u043D\u0430\u0439\u0437\u0435\u0440 \u043F\u043E
|
|
2135
|
+
\u0441\u0435\u043C\u0435\u0439\u0441\u0442\u0432\u0430\u043C (c-\u043F\u043E\u0434\u043E\u0431\u043D\u044B\u0435 \u0441\u043E \u0441\u043B\u044D\u0448-\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u043C\u0438 \u0438 hash-\u044F\u0437\u044B\u043A\u0438 \u0441 \u0440\u0435\u0448\u0451\u0442\u043A\u043E\u0439). \u041C\u043D\u043E\u0433\u043E\u0441\u0442\u0440\u043E\u0447\u043D\u044B\u0435
|
|
2136
|
+
\u0441\u0442\u0440\u043E\u043A\u0438/\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u044B \u0447\u0435\u0440\u0435\u0437 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0441\u0442\u0440\u043E\u043A \u0434\u0438\u0444\u0444\u0430 \u043D\u0435 \u0442\u044F\u043D\u0435\u043C \u2014 \u0434\u043B\u044F \u0436\u0438\u0432\u043E\u0433\u043E \u0432\u0438\u0434\u0430 \u044D\u0442\u043E \u043E\u043A. \u2500\u2500 */
|
|
2137
|
+
var HLKW = {
|
|
2138
|
+
clike: 'const let var function return if else for while do switch case break continue new class extends implements interface type enum export import from as default async await yield try catch finally throw typeof instanceof in of void delete this super null true false undefined public private protected static readonly abstract get set namespace declare package',
|
|
2139
|
+
hash: 'def class return if elif else for while import from as with try except finally raise lambda None True False and or not in is pass break continue yield global nonlocal async await then fi do done case esac function local export echo'
|
|
2140
|
+
};
|
|
2141
|
+
(function(){ for(var k in HLKW){ var s={}; HLKW[k].split(' ').forEach(function(w){ s[w]=1; }); HLKW[k]=s; } })();
|
|
2142
|
+
var HLFAM = {}; (function(){
|
|
2143
|
+
'ts tsx js jsx mjs cjs json jsonc css scss less go rs java kt swift c h cc cpp hpp php scala dart'.split(' ').forEach(function(e){ HLFAM[e]='clike'; });
|
|
2144
|
+
'sh bash zsh py rb yml yaml toml ini env cfg conf dockerfile makefile mk'.split(' ').forEach(function(e){ HLFAM[e]='hash'; });
|
|
2145
|
+
})();
|
|
2146
|
+
/* \u0441\u0442\u0440\u043E\u043A\u043E\u0432\u044B\u0435 \u043B\u0438\u0442\u0435\u0440\u0430\u043B\u044B: "\u2026" '\u2026' \u0438 \u0448\u0430\u0431\u043B\u043E\u043D\u043D\u044B\u0435 \u2014 backtick \u0447\u0435\u0440\u0435\u0437 \\x60, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0437\u0430\u043A\u0440\u044B\u0442\u044C
|
|
2147
|
+
\u043D\u0430\u0448 template literal \u0441\u044B\u0440\u044B\u043C backtick'\u043E\u043C. */
|
|
2148
|
+
var RE_CLIKE = /(\\/\\/.*|\\/\\*[\\s\\S]*?(?:\\*\\/|$))|("(?:\\\\.|[^"\\\\])*"?|'(?:\\\\.|[^'\\\\])*'?|\\x60(?:\\\\.|[^\\x60\\\\])*\\x60?)|(\\b0[xX][0-9a-fA-F]+\\b|\\b\\d[\\d_]*(?:\\.\\d+)?(?:[eE][+-]?\\d+)?\\b)|([A-Za-z_$][\\w$]*)/g;
|
|
2149
|
+
var RE_HASH = /(#.*)|("(?:\\\\.|[^"\\\\])*"?|'(?:\\\\.|[^'\\\\])*'?|\\x60(?:\\\\.|[^\\x60\\\\])*\\x60?)|(\\b0[xX][0-9a-fA-F]+\\b|\\b\\d[\\d_]*(?:\\.\\d+)?(?:[eE][+-]?\\d+)?\\b)|([A-Za-z_$][\\w$]*)/g;
|
|
2150
|
+
function langOf(path){ var p=String(path||'').toLowerCase(); var base=p.split('/').pop()||'';
|
|
2151
|
+
if(base==='dockerfile') return 'hash'; if(base==='makefile') return 'hash';
|
|
2152
|
+
var dot=base.lastIndexOf('.'); var ext=dot>=0?base.slice(dot+1):''; return HLFAM[ext]||'plain'; }
|
|
2153
|
+
function hl(src, lang){ if(lang==='plain'||!src) return E(src);
|
|
2154
|
+
var kw=HLKW[lang]||HLKW.clike; var re=lang==='hash'?RE_HASH:RE_CLIKE; re.lastIndex=0;
|
|
2155
|
+
var out='', last=0, m;
|
|
2156
|
+
while((m=re.exec(src))){
|
|
2157
|
+
if(m.index>last) out+=E(src.slice(last,m.index));
|
|
2158
|
+
var tok=m[0], cls='';
|
|
2159
|
+
if(m[1]!=null) cls='c'; else if(m[2]!=null) cls='s'; else if(m[3]!=null) cls='n';
|
|
2160
|
+
else if(kw[tok]) cls='k';
|
|
2161
|
+
else if(/^[A-Z]/.test(tok)) cls='t';
|
|
2162
|
+
else if(src[re.lastIndex]==='(') cls='f';
|
|
2163
|
+
out += cls ? '<span class="'+cls+'">'+E(tok)+'</span>' : E(tok);
|
|
2164
|
+
last=re.lastIndex;
|
|
2165
|
+
if(m.index===re.lastIndex) re.lastIndex++; // \u0441\u0442\u0440\u0430\u0445\u043E\u0432\u043A\u0430 \u043E\u0442 \u043D\u0443\u043B\u0435\u0432\u043E\u0433\u043E \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u044F
|
|
2166
|
+
}
|
|
2167
|
+
if(last<src.length) out+=E(src.slice(last));
|
|
2168
|
+
return out;
|
|
2169
|
+
}
|
|
2170
|
+
function fileDot(st){ return st==='added'?'+':(st==='deleted'?'\u2212':(st==='renamed'?'\u21C4':'~')); }
|
|
2171
|
+
|
|
2172
|
+
/* \u2500\u2500 \u0436\u0438\u0432\u043E\u0439 \u043A\u043E\u0434 \u0434\u0440\u043E\u043D\u0430: git diff worktree, \u043F\u043E\u043B\u043B\u0438\u043D\u0433 ~2\u0441 (\u0441\u043C. GET /run/:id/diff) \u2500\u2500 */
|
|
2173
|
+
var mView='steps', mDiff=null, mDiffId=null, mSel=null, mPoll=null, mScroll=0;
|
|
2174
|
+
function mStopPoll(){ if(mPoll){ clearInterval(mPoll); mPoll=null; } }
|
|
2175
|
+
function mFetchDiff(){ var id=openId; if(id==null) return;
|
|
2176
|
+
fetch('/run/'+id+'/diff').then(function(r){ return r.json(); }).then(function(d){
|
|
2177
|
+
if(openId!==id) return; mDiff=d; mDiffId=id;
|
|
2178
|
+
if(!d.ok) mStopPoll(); // worktree \u0443\u0431\u0440\u0430\u043D \u2014 \u043F\u043E\u043B\u043B\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E
|
|
2179
|
+
if(mView==='code') paintCode();
|
|
2180
|
+
}).catch(function(){});
|
|
2181
|
+
}
|
|
2182
|
+
function mSetView(v){ mView=v; modal.classList.toggle('wide', v==='code');
|
|
2183
|
+
var st=document.getElementById('m-steps'), cd=document.getElementById('m-code');
|
|
2184
|
+
var ts=document.getElementById('mt-steps'), tc=document.getElementById('mt-code');
|
|
2185
|
+
if(st) st.hidden=(v!=='steps'); if(cd) cd.hidden=(v!=='code');
|
|
2186
|
+
if(ts) ts.classList.toggle('on', v==='steps'); if(tc) tc.classList.toggle('on', v==='code');
|
|
2187
|
+
if(v==='code'){ if(mDiffId!==openId){ mDiff=null; mSel=null; mScroll=0; } paintCode();
|
|
2188
|
+
if(!mPoll){ mFetchDiff(); mPoll=setInterval(mFetchDiff, 2000); } }
|
|
2189
|
+
else mStopPoll();
|
|
2190
|
+
}
|
|
2191
|
+
function renderFileDiff(f){
|
|
2192
|
+
if(f.binary) return '<div class="mc-empty">\u0411\u0438\u043D\u0430\u0440\u043D\u044B\u0439 \u0444\u0430\u0439\u043B \u2014 \u0434\u0438\u0444\u0444 \u043D\u0435 \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u043C.</div>';
|
|
2193
|
+
var lang=langOf(f.path), rows=[];
|
|
2194
|
+
f.hunks.forEach(function(h){
|
|
2195
|
+
rows.push('<span class="mc-hh">'+E(h.header||'\u2026')+'</span>');
|
|
2196
|
+
h.lines.forEach(function(l){
|
|
2197
|
+
var cls=l.k, sign=l.k==='add'?'+':(l.k==='del'?'\u2212':' ');
|
|
2198
|
+
rows.push('<div class="mc-ln '+cls+'"><span class="mc-no">'+(l.o==null?'':l.o)+'</span><span class="mc-no">'+(l.n==null?'':l.n)+
|
|
2199
|
+
'</span><span class="mc-sign">'+sign+'</span><span class="mc-code">'+hl(l.t, lang)+'</span></div>');
|
|
2200
|
+
});
|
|
2201
|
+
});
|
|
2202
|
+
if(f.truncated) rows.push('<div class="mc-more">\u2026 \u0444\u0430\u0439\u043B \u0431\u043E\u043B\u044C\u0448\u043E\u0439 \u2014 \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u0430 \u0447\u0430\u0441\u0442\u044C \u0434\u0438\u0444\u0444\u0430</div>');
|
|
2203
|
+
return rows.join('');
|
|
2204
|
+
}
|
|
2205
|
+
function paintCode(){ var cd=document.getElementById('m-code'); if(!cd) return; var d=mDiff;
|
|
2206
|
+
if(!d){ cd.innerHTML='<div class="mc-empty">\u0427\u0438\u0442\u0430\u044E \u043A\u043E\u0434 \u0434\u0440\u043E\u043D\u0430\u2026</div>'; return; }
|
|
2207
|
+
if(!d.ok){ cd.innerHTML='<div class="mc-empty"><div>'+E(d.reason||'\u0434\u0438\u0444\u0444 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D')+'</div>'+(d.pr?'<a class="mc-prlink" href="'+E(d.pr)+'" target="_blank" rel="noopener">\u041E\u0442\u043A\u0440\u044B\u0442\u044C PR \u2197</a>':'')+'</div>'; return; }
|
|
2208
|
+
if(!d.files.length){ cd.innerHTML='<div class="mc-empty">\u0414\u0440\u043E\u043D \u0435\u0449\u0451 \u043D\u0435 \u0442\u0440\u043E\u043D\u0443\u043B \u043A\u043E\u0434 \u2014 \u043F\u043E\u043A\u0430 \u043F\u0443\u0441\u0442\u043E.</div>'; return; }
|
|
2209
|
+
var sel=null; for(var i=0;i<d.files.length;i++){ if(d.files[i].path===mSel){ sel=d.files[i]; break; } }
|
|
2210
|
+
if(!sel){ sel=d.files[0]; mSel=sel.path; } // \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u2014 \u0442\u0440\u043E\u043D\u0443\u0442\u044B\u0439 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u043C (files[0])
|
|
2211
|
+
var list=d.files.map(function(f){ var on=f.path===mSel;
|
|
2212
|
+
return '<button class="mc-file'+(on?' on':'')+'" data-p="'+E(f.path)+'"><span class="mc-fs '+f.status+'">'+fileDot(f.status)+'</span>'+
|
|
2213
|
+
'<span class="mc-fp" title="'+E(f.path)+'">'+E(f.path)+'</span><span class="mc-fn">'+
|
|
2214
|
+
(f.added?'<span class="a">+'+f.added+'</span>':'')+(f.removed?'<span class="d">\u2212'+f.removed+'</span>':'')+'</span></button>';
|
|
2215
|
+
}).join('');
|
|
2216
|
+
cd.innerHTML='<div class="mc-wrap"><div class="mc-list">'+list+'</div><div class="mc-diff" id="mc-diff">'+renderFileDiff(sel)+'</div></div>'+
|
|
2217
|
+
'<div class="mc-foot">'+(d.base?'<span>\u0431\u0430\u0437\u0430 '+E(d.base)+'</span>':'')+'<span class="grow"></span><span class="a">+'+d.added+'</span><span class="d">\u2212'+d.removed+'</span>'+(d.truncated?'<span> \xB7 \u043D\u0435 \u0432\u0441\u0435 \u0444\u0430\u0439\u043B\u044B</span>':'')+'</div>';
|
|
2218
|
+
cd.querySelectorAll('.mc-file').forEach(function(b){ b.addEventListener('click', function(){ if(b.dataset.p!==mSel){ mSel=b.dataset.p; mScroll=0; paintCode(); } }); });
|
|
2219
|
+
var dp=document.getElementById('mc-diff'); if(dp){ dp.scrollTop=mScroll; dp.addEventListener('scroll', function(){ mScroll=dp.scrollTop; }, {passive:true}); }
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
/* \u2500\u2500 \u043C\u043E\u0434\u0430\u043B\u043A\u0430 (\u0436\u0438\u0432\u0451\u0442 \u043F\u043E SSE) \u2500\u2500 */
|
|
2223
|
+
var overlay=document.getElementById('overlay'), modal=document.getElementById('modal');
|
|
2224
|
+
function node(kind){ if(kind==='fail') return '<span class="node">'+tk('x',2.6)+'</span>'; if(kind==='now') return '<span class="node"></span>'; return '<span class="node">'+tk('check',2.6)+'</span>'; }
|
|
2225
|
+
function stepLabel(e){ if(e.kind==='step') return e.text||''; var b=EVL[e.kind]||e.kind; return e.text ? b+': '+e.text : b; }
|
|
2226
|
+
function firstQid(id){ for(var i=0;i<S.questions.length;i++) if(S.questions[i].run_id===id) return S.questions[i].id; return null; }
|
|
2227
|
+
function openModal(id){ var reopen=(openId===id); openId=id; var d=byId[id]; if(!d){ closeModal(); return; }
|
|
2228
|
+
if(!reopen){ mView='steps'; mDiff=null; mDiffId=null; mSel=null; mScroll=0; mStopPoll(); } // \u0434\u0440\u0443\u0433\u043E\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u2014 \u0441\u0431\u0440\u043E\u0441 \u043A\u043E\u0434\u0430
|
|
2229
|
+
var meta=[]; if(d.branch) meta.push('\u0432\u0435\u0442\u043A\u0430 '+d.branch); if(d.pid!=null) meta.push('pid '+d.pid);
|
|
2230
|
+
if(d.worktree){ var i=d.worktree.indexOf('/worktrees/'); meta.push(i>=0 ? '\u2026'+d.worktree.slice(i) : d.worktree); }
|
|
2231
|
+
meta.push('\u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D '+rel(d.updated));
|
|
2232
|
+
var evs = d.events||[]; var active = !TERM[d.status];
|
|
2233
|
+
var steps = evs.map(function(e,i){ var last=i===evs.length-1;
|
|
2234
|
+
var fail = e.kind==='stalled'||e.kind==='failed'||e.kind==='stopped';
|
|
2235
|
+
var kind = fail ? 'fail' : (last && active ? 'now' : 'done');
|
|
2236
|
+
return '<li class="step '+kind+'">'+node(kind)+'<div><div class="stext">'+E(stepLabel(e))+(kind==='now'?'<span class="live-lbl">\u0441\u0435\u0439\u0447\u0430\u0441</span>':'')+'</div>'+(e.at?'<div class="stime" title="'+E(localFull(e.at))+'">'+E(localHM(e.at))+' \xB7 '+E(rel(e.at))+'</div>':'')+'</div></li>'; }).join('');
|
|
2237
|
+
var url = taskUrl(d);
|
|
2238
|
+
var links = (url?'<a href="'+E(url)+'" target="_blank" rel="noopener">'+E(d.slug)+' \u2197</a>':'<span class="mono">'+E(d.slug)+'</span>')+(d.pr?'<a href="'+E(d.pr)+'" target="_blank" rel="noopener">PR \u2197</a>':'');
|
|
2239
|
+
var desc = d.desc || (d.stopReason?('\u041E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D: '+d.stopReason):('\u0414\u0440\u043E\u043D '+d.name+' \xB7 \u043F\u0440\u043E\u0433\u043E\u043D #'+d.id));
|
|
2240
|
+
var codeBadge = (mDiffId===id && mDiff && mDiff.ok && mDiff.files.length) ? '<span class="mc-badge"><span class="a">+'+mDiff.added+'</span><span class="d">\u2212'+mDiff.removed+'</span></span>' : '';
|
|
2241
|
+
var acts='';
|
|
2242
|
+
var qid = firstQid(d.id);
|
|
2243
|
+
if(d.status==='blocked' && qid!=null) acts += '<button class="abtn" onclick="answerFromModal('+qid+')">\u041E\u0442\u0432\u0435\u0442\u0438\u0442\u044C</button>';
|
|
2244
|
+
if(d.hasDecision) acts += '<button class="abtn" onclick="setTab(\\'history\\')">\u041F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u0442\u044C</button>';
|
|
2245
|
+
acts += '<span class="grow"></span>';
|
|
2246
|
+
if(!TERM[d.status]) acts += '<button class="abtn warn" data-slug="'+E(d.slug)+'" onclick="stopDrone(this.dataset.slug)">\u041E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C</button>';
|
|
2247
|
+
modal.innerHTML =
|
|
2248
|
+
'<div class="m-head">'+ava(d)+'<div><div class="m-name">'+E(d.name)+pill(d.status)+'</div><div class="m-meta">'+meta.map(function(m){return '<span>'+E(m)+'</span>';}).join('')+'</div></div>'+
|
|
2249
|
+
'<button class="m-close" id="mx">'+tk('x',2.4)+'</button></div>'+
|
|
2250
|
+
'<div class="m-body"><div class="taskbox">'+links+'<div class="tt">'+E(d.title || d.slug)+'</div><div class="desc">'+E(desc)+'</div></div>'+
|
|
2251
|
+
'<div class="m-tabs"><div class="seg"><button id="mt-steps" class="'+(mView==='steps'?'on':'')+'" onclick="mSetView(\\'steps\\')">\u0428\u0430\u0433\u0438</button><button id="mt-code" class="'+(mView==='code'?'on':'')+'" onclick="mSetView(\\'code\\')">\u041A\u043E\u0434</button></div>'+codeBadge+'</div>'+
|
|
2252
|
+
'<div id="m-steps"'+(mView!=='steps'?' hidden':'')+'><ul class="steps">'+(steps || '<li class="step"><span class="node"></span><div class="stext" style="color:var(--muted-foreground)">\u0428\u0430\u0433\u043E\u0432 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442.</div></li>')+'</ul></div>'+
|
|
2253
|
+
'<div id="m-code"'+(mView!=='code'?' hidden':'')+'></div></div>'+
|
|
2254
|
+
'<div class="m-actions">'+acts+'</div>';
|
|
2255
|
+
modal.classList.toggle('wide', mView==='code');
|
|
2256
|
+
overlay.classList.add('open'); document.getElementById('mx').addEventListener('click', closeModal);
|
|
2257
|
+
if(mView==='code'){ paintCode(); if(!mPoll){ mFetchDiff(); mPoll=setInterval(mFetchDiff, 2000); } }
|
|
2258
|
+
}
|
|
2259
|
+
function closeModal(){ overlay.classList.remove('open'); openId=null; mStopPoll(); }
|
|
2260
|
+
overlay.addEventListener('click', function(e){ if(e.target===overlay) closeModal(); });
|
|
2261
|
+
document.addEventListener('keydown', function(e){ if(e.key==='Escape') closeModal(); });
|
|
2262
|
+
|
|
2263
|
+
/* \u2500\u2500 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u2500\u2500 */
|
|
2264
|
+
function answerQ(qid, text){ if(!text) return; S.questions = S.questions.filter(function(q){ return q.id!==qid; }); updateBadges();
|
|
2265
|
+
if(TAB==='inbox') renderInbox(); if(openId!=null && byId[openId]) openModal(openId);
|
|
2266
|
+
fetch('/answer', { method:'POST', body:new URLSearchParams({ qid:qid, text:text }) }); }
|
|
2267
|
+
function answerOpt(el){ answerQ(+el.dataset.qid, el.dataset.opt); }
|
|
2268
|
+
function answerFromModal(qid){ closeModal(); setTab('inbox'); var el=document.getElementById('q-'+qid); if(el){ el.scrollIntoView({block:'center'}); var inp=el.querySelector('input[name="text"]'); if(inp) inp.focus(); } }
|
|
2269
|
+
function replayOpt(el){ replayQ(+el.dataset.qid, el.dataset.opt); }
|
|
2270
|
+
function replayQ(qid, opt){ if(!confirm('\u041F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u0442\u044C \u043D\u0430 \xAB'+opt+'\xBB?\\n\u0421\u0442\u0430\u0440\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043F\u043E\u0433\u0430\u0441\u0438\u0442\u0441\u044F (\u0438 \u0435\u0433\u043E PR \u0437\u0430\u043A\u0440\u043E\u0435\u0442\u0441\u044F), \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0441\u044F \u043D\u043E\u0432\u044B\u0439 \u0441 \u044D\u0442\u0438\u043C \u0440\u0435\u0448\u0435\u043D\u0438\u0435\u043C.')) return;
|
|
2271
|
+
fetch('/replay', { method:'POST', body:new URLSearchParams({ qid:qid, option:opt }) }); }
|
|
2272
|
+
function stopDrone(slug){ if(!confirm('\u041E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430 \u043F\u043E \u0437\u0430\u0434\u0430\u0447\u0435 '+slug+'?\\n\u041F\u0440\u043E\u0446\u0435\u0441\u0441 \u043F\u043E\u0433\u0430\u0441\u0438\u0442\u0441\u044F, worktree \u0432\u044B\u0431\u0440\u043E\u0441\u0438\u0442\u0441\u044F, \u043F\u0440\u043E\u0433\u043E\u043D \u0441\u0442\u0430\u043D\u0435\u0442 \xAB\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\xBB.')) return;
|
|
2273
|
+
closeModal(); fetch('/stop', { method:'POST', body:new URLSearchParams({ slug:slug }) }); }
|
|
2274
|
+
|
|
2275
|
+
/* \u2500\u2500 \u0442\u0430\u0431\u044B, ScrollFade, live \u2500\u2500 */
|
|
2276
|
+
function renderTab(){ if(TAB!=='timeline') tlStop();
|
|
2277
|
+
if(TAB==='home') renderHome(); else if(TAB==='fleet') renderFleet(); else if(TAB==='inbox') renderInbox(); else if(TAB==='timeline') tlEnsure(); else renderHistory(); sfUpdate(); }
|
|
2278
|
+
function setTab(t){ TAB=t;
|
|
2279
|
+
document.querySelectorAll('.sb a.item').forEach(function(x){ x.classList.toggle('active', x.dataset.tab===t); });
|
|
2280
|
+
['home','inbox','fleet','timeline','history'].forEach(function(k){ document.getElementById('tab-'+k).hidden=(k!==t); });
|
|
2281
|
+
document.getElementById('sf-scroll').scrollTop=0; renderTab(); }
|
|
2282
|
+
document.querySelectorAll('.sb a.item').forEach(function(a){ a.addEventListener('click', function(){ setTab(a.dataset.tab); }); });
|
|
2283
|
+
|
|
2284
|
+
var sf=document.getElementById('sf'), sfScroll=document.getElementById('sf-scroll');
|
|
2285
|
+
function sfUpdate(){ var top=sfScroll.scrollTop, max=sfScroll.scrollHeight-sfScroll.clientHeight; sf.dataset.canUp=top>1?"1":"0"; sf.dataset.canDown=max-top>1?"1":"0"; }
|
|
2286
|
+
sfScroll.addEventListener('scroll', sfUpdate, {passive:true}); new ResizeObserver(sfUpdate).observe(sfScroll); window.addEventListener('resize', sfUpdate);
|
|
2287
|
+
|
|
2288
|
+
function updateBadges(){ document.getElementById('b-inbox').textContent = S.questions.length ? S.questions.length : ''; document.getElementById('b-fleet').textContent = S.metrics.running ? S.metrics.running : ''; }
|
|
2289
|
+
/* \u0428\u0442\u043E\u0440\u043C\u043E\u0432\u043E\u0439 \u0431\u0430\u043D\u043D\u0435\u0440 (TASWM-23): \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \xAB\u0440\u043E\u0439 \u043D\u0430 \u043F\u0430\u0443\u0437\u0435\xBB, \u043F\u043E\u043A\u0430 \u043B\u0435\u0436\u0438\u0442 Anthropic. */
|
|
2290
|
+
function paintStorm(){ var b=document.getElementById('storm-banner'); if(!b) return;
|
|
2291
|
+
var st=S.storm||{active:false};
|
|
2292
|
+
if(!st.active){ b.hidden=true; b.innerHTML=''; return; }
|
|
2293
|
+
var since = st.since ? ' \xB7 \u0441 '+rel(st.since) : '';
|
|
2294
|
+
var frozen = st.dead ? st.dead+' '+plural(st.dead,DRONEW)+' \u0432 \u043A\u043E\u043A\u043E\u043D\u0435' : '\u043D\u043E\u0432\u044B\u0445 \u0434\u0440\u043E\u043D\u043E\u0432 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u043C';
|
|
2295
|
+
b.innerHTML='<span class="sb-ico">\u26A1</span><div class="sb-body"><div class="sb-t">\u041F\u0441\u0438\u043E\u043D\u043D\u0430\u044F \u0441\u0432\u044F\u0437\u044C \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430 \u2014 Anthropic \u0448\u0442\u043E\u0440\u043C\u0438\u0442</div>'+
|
|
2296
|
+
'<div class="sb-d">\u0420\u043E\u0439 \u0437\u0430\u043C\u0435\u0440, \u0434\u0440\u043E\u043D\u044B \u0432 \u043A\u043E\u043A\u043E\u043D\u0430\u0445 \u2014 '+E(frozen)+', \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u043A\u043E\u043F\u0438\u0442\u0441\u044F'+E(since)+'. \u041F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F \u0441\u0430\u043C, \u043A\u0430\u043A \u0441\u0432\u044F\u0437\u044C \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F.</div></div>';
|
|
2297
|
+
b.hidden=false; }
|
|
2298
|
+
function applySnap(s){ S=s; byId={}; s.runs.forEach(function(d){ byId[d.id]=d; }); updateBadges(); paintStorm(); renderTab(); if(openId!=null && byId[openId]) openModal(openId); }
|
|
2299
|
+
|
|
2300
|
+
/* \u0434\u0435\u043B\u0435\u0433\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \u0441\u0430\u0431\u043C\u0438\u0442 \u0444\u043E\u0440\u043C (spawn + answer) */
|
|
2301
|
+
document.addEventListener('submit', function(ev){ var f=ev.target; if(!f.matches || !f.matches('form[data-ajax]')) return; ev.preventDefault();
|
|
2302
|
+
var body=new URLSearchParams(new FormData(f));
|
|
2303
|
+
if(f.getAttribute('action')==='/answer'){ var qid=+body.get('qid'); var text=(body.get('text')||'').trim(); if(text) answerQ(qid, text); f.reset(); return; }
|
|
2304
|
+
fetch(f.getAttribute('action'), { method:'POST', body:body }); f.reset(); });
|
|
2305
|
+
|
|
2306
|
+
applySnap(S);
|
|
2307
|
+
var es = new EventSource('/events');
|
|
2308
|
+
es.onmessage = function(e){ document.getElementById('conn').classList.remove('off'); document.getElementById('conn').textContent='overmind \xB7 ${esc(local)}'; applySnap(JSON.parse(e.data)); };
|
|
2309
|
+
es.onerror = function(){ var c=document.getElementById('conn'); c.classList.add('off'); c.textContent='\u043D\u0435\u0442 \u0441\u0432\u044F\u0437\u0438 \u2014 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u044E\u0441\u044C\u2026'; };
|
|
2310
|
+
</script>`);
|
|
2311
|
+
}
|
|
2312
|
+
var html = (body) => new Response(body, { headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
2313
|
+
var redirect = (to) => new Response(null, { status: 302, headers: { Location: to } });
|
|
2314
|
+
var pending = /* @__PURE__ */ new Map();
|
|
2315
|
+
async function formValue(req) {
|
|
2316
|
+
return new URLSearchParams(await req.text());
|
|
2317
|
+
}
|
|
2318
|
+
var diffCache = /* @__PURE__ */ new Map();
|
|
2319
|
+
var DIFF_TTL = 1500;
|
|
2320
|
+
async function runDiff(db2, id, now = Date.now()) {
|
|
2321
|
+
const run = db2.getRun(id);
|
|
2322
|
+
if (!run) return null;
|
|
2323
|
+
const hit = diffCache.get(id);
|
|
2324
|
+
if (hit && now - hit.at < DIFF_TTL) return hit.payload;
|
|
2325
|
+
const payload = await collectRunDiff(run.worktree_path, { pr: run.pr_url, status: run.status });
|
|
2326
|
+
diffCache.set(id, { at: now, payload });
|
|
2327
|
+
return payload;
|
|
2328
|
+
}
|
|
2329
|
+
function eventsStream(db2) {
|
|
2330
|
+
let iv;
|
|
2331
|
+
let last = "";
|
|
2332
|
+
const stream = new ReadableStream({
|
|
2333
|
+
start(controller) {
|
|
2334
|
+
const enc = new TextEncoder();
|
|
2335
|
+
const send = () => {
|
|
2336
|
+
const snap = JSON.stringify(roySnapshot(db2));
|
|
2337
|
+
if (snap === last) return;
|
|
2338
|
+
last = snap;
|
|
2339
|
+
try {
|
|
2340
|
+
controller.enqueue(enc.encode(`data: ${snap}
|
|
2341
|
+
|
|
2342
|
+
`));
|
|
2343
|
+
} catch {
|
|
2344
|
+
if (iv) clearInterval(iv);
|
|
2345
|
+
}
|
|
2346
|
+
};
|
|
2347
|
+
send();
|
|
2348
|
+
iv = setInterval(send, 1e3);
|
|
2349
|
+
},
|
|
2350
|
+
cancel() {
|
|
2351
|
+
if (iv) clearInterval(iv);
|
|
2352
|
+
}
|
|
2353
|
+
});
|
|
2354
|
+
return new Response(stream, {
|
|
2355
|
+
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" }
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
function makeHandler(deps) {
|
|
2359
|
+
const api = deps.api ?? TASKLESS_API;
|
|
2360
|
+
const redirectUri = deps.redirectUri ?? WEB_REDIRECT_URI;
|
|
2361
|
+
const auth = deps.auth ?? loadStore;
|
|
2362
|
+
const defaultRepo = normalizeRepo(deps.defaultRepo ?? SWARM_REPO);
|
|
2363
|
+
const taskUrlBase = deps.taskUrlBase ?? TASKLESS_TASK_URL_BASE;
|
|
2364
|
+
const unauthorized = () => new Response("\u043D\u0443\u0436\u0435\u043D \u0432\u0445\u043E\u0434 \u0447\u0435\u0440\u0435\u0437 Taskless", { status: 401 });
|
|
2365
|
+
return async function handle(req) {
|
|
2366
|
+
const url = new URL(req.url);
|
|
2367
|
+
const p = url.pathname;
|
|
2368
|
+
if (p === "/") {
|
|
2369
|
+
const store = auth();
|
|
2370
|
+
return html(store ? renderDashboard(store.email, roySnapshot(deps.db), taskUrlBase) : renderLogin());
|
|
2371
|
+
}
|
|
2372
|
+
if (p === "/events") return eventsStream(deps.db);
|
|
2373
|
+
if (p === "/state") return Response.json(roySnapshot(deps.db));
|
|
2374
|
+
if (p === "/timeline") return Response.json(timelineState(deps.db));
|
|
2375
|
+
const diffMatch = p.match(/^\/run\/(\d+)\/diff$/);
|
|
2376
|
+
if (diffMatch) {
|
|
2377
|
+
if (!auth()) return unauthorized();
|
|
2378
|
+
const payload = await runDiff(deps.db, Number(diffMatch[1]));
|
|
2379
|
+
if (!payload) return new Response("run not found", { status: 404 });
|
|
2380
|
+
return Response.json(payload);
|
|
2381
|
+
}
|
|
2382
|
+
if (p === "/login") {
|
|
2383
|
+
const clientId = await ensureClientId(api, redirectUri);
|
|
2384
|
+
const { verifier, challenge } = makePkce();
|
|
2385
|
+
const state = randomBytes2(16).toString("base64url");
|
|
2386
|
+
pending.set(state, verifier);
|
|
2387
|
+
return redirect(authorizeUrl({ api, clientId, redirectUri, challenge, state }));
|
|
2388
|
+
}
|
|
2389
|
+
if (p === "/callback") {
|
|
2390
|
+
const code = url.searchParams.get("code");
|
|
2391
|
+
const state = url.searchParams.get("state");
|
|
2392
|
+
const verifier = state ? pending.get(state) : void 0;
|
|
2393
|
+
const clientId = loadClientId();
|
|
2394
|
+
if (!code || !verifier || !clientId) return html(pageShell(`<div class="login-wrap"><div class="login-card"><h1>\u0412\u0425\u041E\u0414 \u041D\u0415 \u0423\u0414\u0410\u041B\u0421\u042F</h1><a class="btn-tl" href="/">\u2190 \u043D\u0430\u0437\u0430\u0434</a></div></div>`));
|
|
2395
|
+
pending.delete(state);
|
|
2396
|
+
try {
|
|
2397
|
+
const t = await exchangeCode({ api, clientId, redirectUri, code, verifier });
|
|
2398
|
+
saveStore(toStore(clientId, t, Date.now()));
|
|
2399
|
+
} catch (e) {
|
|
2400
|
+
return html(pageShell(`<div class="login-wrap"><div class="login-card"><h1>\u041E\u0411\u041C\u0415\u041D \u041A\u041E\u0414\u0410 \u041D\u0415 \u0423\u0414\u0410\u041B\u0421\u042F</h1><p>${esc(e instanceof Error ? e.message : String(e))}</p><a class="btn-tl" href="/">\u2190 \u043D\u0430\u0437\u0430\u0434</a></div></div>`));
|
|
2401
|
+
}
|
|
2402
|
+
return redirect("/");
|
|
2403
|
+
}
|
|
2404
|
+
if (p === "/logout") {
|
|
2405
|
+
clearStore();
|
|
2406
|
+
return redirect("/");
|
|
2407
|
+
}
|
|
2408
|
+
if (p === "/spawn" && req.method === "POST") {
|
|
2409
|
+
if (!auth()) return unauthorized();
|
|
2410
|
+
const f = await formValue(req);
|
|
2411
|
+
const slug = (f.get("slug") ?? "").trim();
|
|
2412
|
+
const area = (f.get("area") ?? "").trim() || null;
|
|
2413
|
+
const rawRepo = (f.get("repo") ?? "").trim();
|
|
2414
|
+
const repo = rawRepo ? normalizeRepo(rawRepo) : null;
|
|
2415
|
+
if (repo) {
|
|
2416
|
+
const allowed = new Set([...deps.db.distinctRepos(), defaultRepo].filter(Boolean).map(normalizeRepo));
|
|
2417
|
+
if (!allowed.has(repo)) return new Response(`repo \u043D\u0435 \u0432 \u0441\u043F\u0438\u0441\u043A\u0435 \u0440\u043E\u044F: ${repo}`, { status: 400 });
|
|
2418
|
+
}
|
|
2419
|
+
if (slug) deps.db.createRun({ task_slug: slug, area, repo });
|
|
2420
|
+
return redirect("/");
|
|
2421
|
+
}
|
|
2422
|
+
if (p === "/answer" && req.method === "POST") {
|
|
2423
|
+
if (!auth()) return unauthorized();
|
|
2424
|
+
const f = await formValue(req);
|
|
2425
|
+
const qid = Number(f.get("qid"));
|
|
2426
|
+
const text = (f.get("text") ?? "").trim();
|
|
2427
|
+
if (qid && text) deps.db.answer(qid, text, "human");
|
|
2428
|
+
return redirect("/");
|
|
2429
|
+
}
|
|
2430
|
+
if (p === "/replay" && req.method === "POST") {
|
|
2431
|
+
if (!auth()) return unauthorized();
|
|
2432
|
+
const f = await formValue(req);
|
|
2433
|
+
const qid = Number(f.get("qid"));
|
|
2434
|
+
const option = (f.get("option") ?? "").trim();
|
|
2435
|
+
if (qid && option && deps.replay) await deps.replay(qid, option);
|
|
2436
|
+
return redirect("/");
|
|
2437
|
+
}
|
|
2438
|
+
if (p === "/stop" && req.method === "POST") {
|
|
2439
|
+
if (!auth()) return unauthorized();
|
|
2440
|
+
const f = await formValue(req);
|
|
2441
|
+
const slug = (f.get("slug") ?? "").trim();
|
|
2442
|
+
const reason = (f.get("reason") ?? "").trim() || "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0438\u0437 \u0432\u0435\u0431\u0430";
|
|
2443
|
+
if (slug && deps.stop) await deps.stop(slug, reason);
|
|
2444
|
+
return redirect("/");
|
|
2445
|
+
}
|
|
2446
|
+
return new Response("not found", { status: 404 });
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
function startWeb(deps, port = WEB_PORT) {
|
|
2450
|
+
const handle = makeHandler(deps);
|
|
2451
|
+
const server = serve({ fetch: handle, port, hostname: "127.0.0.1" });
|
|
2452
|
+
return { port, server };
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
// src/cli.ts
|
|
2456
|
+
import { cancelRun, respawnRun, OVERMIND_ENTRY } from "./overmind.js";
|
|
2457
|
+
|
|
2458
|
+
// src/launcher.ts
|
|
2459
|
+
import { spawn } from "child_process";
|
|
2460
|
+
import { readdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2461
|
+
import { homedir as homedir2 } from "os";
|
|
2462
|
+
import { join as join4 } from "path";
|
|
2463
|
+
function claudeArgs(cfgPath, prompt, continueSession = false) {
|
|
2464
|
+
const base2 = ["--dangerously-skip-permissions", "--mcp-config", cfgPath, "-p", prompt];
|
|
2465
|
+
return continueSession ? ["--continue", ...base2] : base2;
|
|
2466
|
+
}
|
|
2467
|
+
function makeLauncher(cfg) {
|
|
2468
|
+
return ({ worktree, run, prompt, continueSession }) => {
|
|
2469
|
+
const mcpConfig = {
|
|
2470
|
+
mcpServers: {
|
|
2471
|
+
overmind: {
|
|
2472
|
+
command: "bun",
|
|
2473
|
+
args: ["run", cfg.overmindEntry, "--drone", "--run", String(run.id)],
|
|
2474
|
+
env: { SWARM_DB: cfg.dbPath }
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
};
|
|
2478
|
+
const cfgPath = join4(worktree, ".swarm-mcp.json");
|
|
2479
|
+
writeFileSync2(cfgPath, JSON.stringify(mcpConfig, null, 2));
|
|
2480
|
+
const child = spawn(cfg.claudeBin ?? "claude", claudeArgs(cfgPath, prompt, continueSession), {
|
|
2481
|
+
cwd: worktree,
|
|
2482
|
+
detached: true,
|
|
2483
|
+
stdio: "ignore"
|
|
2484
|
+
});
|
|
2485
|
+
child.unref();
|
|
2486
|
+
if (child.pid == null) throw new Error("\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u043D\u0435\u0442 pid)");
|
|
2487
|
+
return child.pid;
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
// src/git.ts
|
|
2492
|
+
import { $ as $3 } from "execa";
|
|
2493
|
+
async function removeWorktree(repo, path) {
|
|
2494
|
+
const branch = (await $3({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
|
|
2495
|
+
await $3({ reject: false })`git -C ${repo} worktree remove --force ${path}`;
|
|
2496
|
+
if (branch) await $3({ reject: false })`git -C ${repo} branch -D ${branch}`;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// src/taskless.ts
|
|
2500
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2501
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
2502
|
+
|
|
2503
|
+
// src/workspace.ts
|
|
2504
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
|
|
2505
|
+
import { join as join5 } from "path";
|
|
2506
|
+
var WORKSPACE_PATH = join5(SWARM_HOME, "workspace.json");
|
|
2507
|
+
function loadWorkspace() {
|
|
2508
|
+
try {
|
|
2509
|
+
const w = JSON.parse(readFileSync3(WORKSPACE_PATH, "utf8"));
|
|
2510
|
+
return w && typeof w.id === "string" && w.id ? w : null;
|
|
2511
|
+
} catch {
|
|
2512
|
+
return null;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
function saveWorkspace(w) {
|
|
2516
|
+
writeFileSync3(WORKSPACE_PATH, JSON.stringify(w, null, 2), { mode: 384 });
|
|
2517
|
+
}
|
|
2518
|
+
function clearWorkspace() {
|
|
2519
|
+
try {
|
|
2520
|
+
unlinkSync2(WORKSPACE_PATH);
|
|
2521
|
+
} catch {
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
function resolvedWorkspaceId() {
|
|
2525
|
+
return TASKLESS_WORKSPACE || loadWorkspace()?.id || "";
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
// src/taskless.ts
|
|
2529
|
+
async function connectSwarmMcp(cfg) {
|
|
2530
|
+
const api = cfg?.api ?? TASKLESS_API;
|
|
2531
|
+
const workspace = cfg?.workspace ?? resolvedWorkspaceId();
|
|
2532
|
+
const ws = workspace ? { workspaceId: workspace } : {};
|
|
2533
|
+
let token = cfg?.token ?? "";
|
|
2534
|
+
if (!token) {
|
|
2535
|
+
const store = loadStore();
|
|
2536
|
+
if (store) {
|
|
2537
|
+
const r = await ensureAccess(store, { api });
|
|
2538
|
+
if (r) {
|
|
2539
|
+
token = r.accessToken;
|
|
2540
|
+
if (r.store) saveStore(r.store);
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
if (!token) token = TASKLESS_TOKEN;
|
|
2545
|
+
if (!token) return null;
|
|
2546
|
+
const url = new URL(`${api.replace(/\/$/, "")}/api/mcp`);
|
|
2547
|
+
const transport = new StreamableHTTPClientTransport(url, {
|
|
2548
|
+
requestInit: { headers: { Authorization: `Bearer ${token}` } }
|
|
2549
|
+
});
|
|
2550
|
+
const client = new Client({ name: "taskless-swarm-client", version: "0.1.0" }, { capabilities: {} });
|
|
2551
|
+
try {
|
|
2552
|
+
await client.connect(transport);
|
|
2553
|
+
} catch (e) {
|
|
2554
|
+
console.warn(`Swarm MCP: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 \u0431\u0443\u0444\u0435\u0440 \u0432 outbox.`);
|
|
2555
|
+
return null;
|
|
2556
|
+
}
|
|
2557
|
+
return async (name, args) => await client.callTool({ name, arguments: { ...args, ...ws } });
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// src/login.ts
|
|
2561
|
+
import { execa } from "execa";
|
|
2562
|
+
async function waitForLogin(d) {
|
|
2563
|
+
const deadline = d.now() + d.timeoutMs;
|
|
2564
|
+
for (; ; ) {
|
|
2565
|
+
const s = d.loadStore();
|
|
2566
|
+
if (s?.accessToken && s.accessToken !== d.beforeToken) return s;
|
|
2567
|
+
if (d.now() >= deadline) return null;
|
|
2568
|
+
await d.sleep(d.pollMs);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
async function chooseWorkspace(d) {
|
|
2572
|
+
if (d.list.length === 0) {
|
|
2573
|
+
d.out("\u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u043E\u0432 (list_workspaces \u043F\u0443\u0441\u0442)");
|
|
2574
|
+
return null;
|
|
2575
|
+
}
|
|
2576
|
+
if (d.arg) {
|
|
2577
|
+
const m = d.list.find((w) => w.id === d.arg || w.slug === d.arg);
|
|
2578
|
+
if (!m) d.out(`\u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${d.arg}. \u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B: ${d.list.map((w) => w.slug).join(", ")}`);
|
|
2579
|
+
return m ?? null;
|
|
2580
|
+
}
|
|
2581
|
+
if (d.list.length === 1) return d.list[0];
|
|
2582
|
+
const preferredIdx = d.preferredId ? d.list.findIndex((w) => w.id === d.preferredId) : -1;
|
|
2583
|
+
if (d.prompt) {
|
|
2584
|
+
d.list.forEach(
|
|
2585
|
+
(w, i) => d.out(` ${i + 1}. ${w.name} (${w.slug})${w.role ? ` \u2014 ${w.role}` : ""}${i === preferredIdx ? " \u2190" : ""}`)
|
|
2586
|
+
);
|
|
2587
|
+
const def = preferredIdx >= 0 ? preferredIdx + 1 : 1;
|
|
2588
|
+
const raw = (await d.prompt(`\u0432\u044B\u0431\u0435\u0440\u0438 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 [${def}]: `)).trim();
|
|
2589
|
+
const n = raw ? Number(raw) : def;
|
|
2590
|
+
if (!Number.isInteger(n) || n < 1 || n > d.list.length) {
|
|
2591
|
+
d.out(`\u043D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 \u0432\u044B\u0431\u043E\u0440: ${raw}`);
|
|
2592
|
+
return null;
|
|
2593
|
+
}
|
|
2594
|
+
return d.list[n - 1];
|
|
2595
|
+
}
|
|
2596
|
+
if (preferredIdx >= 0) return d.list[preferredIdx];
|
|
2597
|
+
d.out(`\u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u043E\u0432 \u2014 \u0443\u043A\u0430\u0436\u0438 --workspace <slug>: ${d.list.map((w) => w.slug).join(", ")}`);
|
|
2598
|
+
return null;
|
|
2599
|
+
}
|
|
2600
|
+
function toWorkspaceSel(w) {
|
|
2601
|
+
return { id: w.id, slug: w.slug, name: w.name };
|
|
2602
|
+
}
|
|
2603
|
+
function jsonFromResult(res) {
|
|
2604
|
+
const text = res?.content?.find((c) => c.type === "text")?.text;
|
|
2605
|
+
if (!text) return res?.structuredContent ?? null;
|
|
2606
|
+
try {
|
|
2607
|
+
return JSON.parse(text);
|
|
2608
|
+
} catch {
|
|
2609
|
+
return null;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
function parseWorkspaces(res) {
|
|
2613
|
+
const arr = jsonFromResult(res);
|
|
2614
|
+
if (!Array.isArray(arr)) return [];
|
|
2615
|
+
return arr.filter((w) => !!w && typeof w.id === "string" && typeof w.slug === "string").map((w) => ({ id: w.id, slug: w.slug, name: typeof w.name === "string" ? w.name : w.slug, role: w.role }));
|
|
2616
|
+
}
|
|
2617
|
+
function parseWhoAmI(res) {
|
|
2618
|
+
const o = jsonFromResult(res);
|
|
2619
|
+
if (!o || typeof o !== "object") return {};
|
|
2620
|
+
const r = o;
|
|
2621
|
+
return {
|
|
2622
|
+
...typeof r.email === "string" ? { email: r.email } : {},
|
|
2623
|
+
...typeof r.defaultWorkspaceId === "string" ? { defaultWorkspaceId: r.defaultWorkspaceId } : {}
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
async function openBrowser(url) {
|
|
2627
|
+
const [cmd2, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
2628
|
+
try {
|
|
2629
|
+
await execa(cmd2, args, { stdio: "ignore" });
|
|
2630
|
+
return true;
|
|
2631
|
+
} catch {
|
|
2632
|
+
return false;
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
// src/cli.ts
|
|
2637
|
+
function takeRepo(argv) {
|
|
2638
|
+
const i = argv.indexOf("--repo");
|
|
2639
|
+
if (i === -1) return { repo: null, rest: argv };
|
|
2640
|
+
const repo = argv[i + 1] ?? null;
|
|
2641
|
+
return { repo, rest: [...argv.slice(0, i), ...argv.slice(i + 2)] };
|
|
2642
|
+
}
|
|
2643
|
+
function takeAfter(argv) {
|
|
2644
|
+
const i = argv.indexOf("--after");
|
|
2645
|
+
if (i === -1) return { after: [], rest: argv };
|
|
2646
|
+
const raw = argv[i + 1] ?? "";
|
|
2647
|
+
const after = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2648
|
+
return { after, rest: [...argv.slice(0, i), ...argv.slice(i + 2)] };
|
|
2649
|
+
}
|
|
2650
|
+
function takeHero(argv) {
|
|
2651
|
+
const i = argv.indexOf("--hero");
|
|
2652
|
+
if (i === -1) return { hero: false, rest: argv };
|
|
2653
|
+
return { hero: true, rest: [...argv.slice(0, i), ...argv.slice(i + 1)] };
|
|
2654
|
+
}
|
|
2655
|
+
function takeName(argv) {
|
|
2656
|
+
const i = argv.indexOf("--name");
|
|
2657
|
+
if (i === -1) return { name: null, rest: argv };
|
|
2658
|
+
const name = argv[i + 1] ?? null;
|
|
2659
|
+
return { name, rest: [...argv.slice(0, i), ...argv.slice(i + 2)] };
|
|
2660
|
+
}
|
|
2661
|
+
function takeWorkspace(argv) {
|
|
2662
|
+
const i = argv.indexOf("--workspace");
|
|
2663
|
+
if (i === -1) return { workspace: null, rest: argv };
|
|
2664
|
+
const workspace = argv[i + 1] ?? null;
|
|
2665
|
+
return { workspace, rest: [...argv.slice(0, i), ...argv.slice(i + 2)] };
|
|
2666
|
+
}
|
|
2667
|
+
async function askTty(q) {
|
|
2668
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2669
|
+
try {
|
|
2670
|
+
return await rl.question(q);
|
|
2671
|
+
} finally {
|
|
2672
|
+
rl.close();
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
async function runLogin(db2, opts) {
|
|
2676
|
+
const before = loadStore();
|
|
2677
|
+
const web = startWeb({ db: db2 });
|
|
2678
|
+
try {
|
|
2679
|
+
let store = before;
|
|
2680
|
+
if (!before) {
|
|
2681
|
+
const loginUrl = `http://127.0.0.1:${web.port}/login`;
|
|
2682
|
+
if (!opts.noOpen && await openBrowser(loginUrl)) console.log(`\u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043E\u0442\u043A\u0440\u044B\u0442: ${loginUrl}`);
|
|
2683
|
+
else console.log("\u043E\u0442\u043A\u0440\u043E\u0439 \u0441\u0441\u044B\u043B\u043A\u0443 \u0432\u0445\u043E\u0434\u0430 \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435 (\u043C\u043E\u0436\u043D\u043E \u043D\u0430 \u0434\u0440\u0443\u0433\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u2014 \u043F\u0440\u043E\u0431\u0440\u043E\u0441 \u043F\u043E\u0440\u0442\u0430: ssh -L 7420:127.0.0.1:7420 \u2026):");
|
|
2684
|
+
console.log(` ${loginUrl}`);
|
|
2685
|
+
console.log("\u0436\u0434\u0443 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0432\u0445\u043E\u0434\u0430\u2026");
|
|
2686
|
+
store = await waitForLogin({
|
|
2687
|
+
loadStore,
|
|
2688
|
+
beforeToken: null,
|
|
2689
|
+
now: Date.now,
|
|
2690
|
+
sleep,
|
|
2691
|
+
timeoutMs: 5 * 6e4,
|
|
2692
|
+
pollMs: 1e3
|
|
2693
|
+
});
|
|
2694
|
+
if (!store) {
|
|
2695
|
+
console.error("\u0432\u0445\u043E\u0434 \u043D\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D (\u0442\u0430\u0439\u043C\u0430\u0443\u0442 5 \u043C\u0438\u043D). \u041F\u043E\u0432\u0442\u043E\u0440\u0438: swarm login");
|
|
2696
|
+
return 1;
|
|
2697
|
+
}
|
|
2698
|
+
} else {
|
|
2699
|
+
console.log(`\u0441\u0435\u0441\u0441\u0438\u044F \u0443\u0436\u0435 \u0435\u0441\u0442\u044C${before.email ? ` (\u0432\u043E\u0448\u0451\u043B \u043A\u0430\u043A ${before.email})` : ""} \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u044E \u0432\u044B\u0431\u043E\u0440 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430. \u041F\u0435\u0440\u0435\u043B\u043E\u0433\u0438\u043D: swarm logout && swarm login`);
|
|
2700
|
+
}
|
|
2701
|
+
console.log(`\u0432\u043E\u0448\u0451\u043B \u043A\u0430\u043A ${store?.email ?? "?"}`);
|
|
2702
|
+
const call = await connectSwarmMcp();
|
|
2703
|
+
if (!call) {
|
|
2704
|
+
console.error("\u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043A Taskless MCP \u0434\u043B\u044F \u0432\u044B\u0431\u043E\u0440\u0430 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430 \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435 \u0438\u043B\u0438 \u0437\u0430\u0434\u0430\u0439 TASKLESS_WORKSPACE \u0432\u0440\u0443\u0447\u043D\u0443\u044E");
|
|
2705
|
+
return 1;
|
|
2706
|
+
}
|
|
2707
|
+
const list = parseWorkspaces(await call("list_workspaces", {}));
|
|
2708
|
+
const who = parseWhoAmI(await call("who_am_i", {}));
|
|
2709
|
+
const chosen = await chooseWorkspace({
|
|
2710
|
+
list,
|
|
2711
|
+
preferredId: who.defaultWorkspaceId ?? null,
|
|
2712
|
+
arg: opts.workspace,
|
|
2713
|
+
prompt: process.stdin.isTTY ? askTty : null,
|
|
2714
|
+
out: (s) => console.log(s)
|
|
2715
|
+
});
|
|
2716
|
+
if (!chosen) return 1;
|
|
2717
|
+
saveWorkspace(toWorkspaceSel(chosen));
|
|
2718
|
+
console.log(`\u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441: ${chosen.name} (${chosen.slug}) \u2192 ${SWARM_HOME}/workspace.json`);
|
|
2719
|
+
console.log("\u0433\u043E\u0442\u043E\u0432\u043E. \u0414\u0435\u043C\u043E\u043D \u0432 \u0442\u043E\u043D\u043A\u043E\u043C \u043A\u043B\u0438\u0435\u043D\u0442\u0435: SWARM_BACKEND=client pnpm --filter @taskless/swarm-daemon start");
|
|
2720
|
+
return 0;
|
|
2721
|
+
} finally {
|
|
2722
|
+
web.server.close();
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
var STATUS_MARK = {
|
|
2726
|
+
queued: "\xB7",
|
|
2727
|
+
running: "\u25B6",
|
|
2728
|
+
blocked: "\u25A0",
|
|
2729
|
+
paused: "\u{1F4A4}",
|
|
2730
|
+
review: "\u25C6",
|
|
2731
|
+
done: "\u2713",
|
|
2732
|
+
failed: "\u2715",
|
|
2733
|
+
stalled: "\u2026",
|
|
2734
|
+
stopped: "\u2298",
|
|
2735
|
+
superseded: "\u293F"
|
|
2736
|
+
};
|
|
2737
|
+
function fmtRun(r) {
|
|
2738
|
+
const mark = STATUS_MARK[r.status] ?? "?";
|
|
2739
|
+
const area = r.area ? ` [${r.area}]` : "";
|
|
2740
|
+
const repo = r.repo ? ` (${basename2(r.repo)})` : "";
|
|
2741
|
+
const q = r.status === "blocked" ? " \u2190 \u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430" : "";
|
|
2742
|
+
const why = r.status === "stopped" && r.stop_reason ? ` \u2014 ${r.stop_reason}` : "";
|
|
2743
|
+
return `${mark} #${r.id} ${r.name} ${r.task_slug}${repo}${area} ${r.status}${q}${why}`;
|
|
2744
|
+
}
|
|
2745
|
+
function usage() {
|
|
2746
|
+
console.log(`swarm \u2014 \u0440\u043E\u0439 \u0434\u0440\u043E\u043D\u043E\u0432
|
|
2747
|
+
|
|
2748
|
+
swarm ls [status] \u0441\u043F\u0438\u0441\u043E\u043A \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432 (\u043E\u043F\u0446. \u0444\u0438\u043B\u044C\u0442\u0440 \u043F\u043E \u0441\u0442\u0430\u0442\u0443\u0441\u0443)
|
|
2749
|
+
swarm spawn <slug> [area] [--repo <path>] [--after <s1,s2>] \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u044C (--after \u2192 \u0436\u0434\u0430\u0442\u044C done \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439)
|
|
2750
|
+
swarm spawn <slug> --hero [--name HYDRA] [--repo <path>] [--after <s1,s2>] \u0413\u0435\u0440\u043E\u0439-\u043B\u0435\u0439\u0442\u0435\u043D\u0430\u043D\u0442 \u044D\u043F\u0438\u043A\u0430
|
|
2751
|
+
swarm wake <runId> \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442\u044C \u0437\u0430\u043C\u0435\u0440\u0448\u0438\u0439 \u043F\u0440\u043E\u0433\u043E\u043D (--continue) \u2014 \u043D\u0430\u043F\u0440. \u0440\u0443\u0447\u043D\u0430\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0413\u0435\u0440\u043E\u044F
|
|
2752
|
+
swarm questions \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0434\u0440\u043E\u043D\u043E\u0432
|
|
2753
|
+
swarm answer <qid> <...> \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u043D\u0430 \u0432\u043E\u043F\u0440\u043E\u0441 (\u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442 \u0434\u0440\u043E\u043D\u0430)
|
|
2754
|
+
swarm cancel <slug> <\u043F\u0440\u0438\u0447\u0438\u043D\u0430\u2026> \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u0441\u0442\u0430\u0442\u0443\u0441 \xAB\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\xBB + \u043F\u0440\u0438\u0447\u0438\u043D\u0430; \u0432 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u043E\u0441\u0442\u0430\u0451\u0442\u0441\u044F)
|
|
2755
|
+
swarm import-db <path> --repo <path> \u0438\u043C\u043F\u043E\u0440\u0442 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u0438\u0437 \u0434\u0440\u0443\u0433\u043E\u0439 \u0411\u0414 (\u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u043D\u043E)
|
|
2756
|
+
swarm log <runId> \u043B\u043E\u0433 \u043F\u0440\u043E\u0433\u043E\u043D\u0430
|
|
2757
|
+
swarm login [--workspace <slug|id>] [--no-open] \u0432\u0445\u043E\u0434 \u0432 \u043F\u0440\u043E\u0434 + \u0432\u044B\u0431\u043E\u0440 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430
|
|
2758
|
+
swarm logout \u0437\u0430\u0431\u044B\u0442\u044C \u0442\u043E\u043A\u0435\u043D\u044B \u0438 \u0432\u044B\u0431\u043E\u0440 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430
|
|
2759
|
+
swarm whoami \u043A\u0442\u043E \u0432\u043E\u0448\u0451\u043B \u0438 \u0432 \u043A\u0430\u043A\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435
|
|
2760
|
+
swarm web \u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0430 (\u0432\u0445\u043E\u0434 + \u0441\u0442\u0430\u0442\u0443\u0441) \u043D\u0430 127.0.0.1
|
|
2761
|
+
`);
|
|
2762
|
+
}
|
|
2763
|
+
var db = new SwarmDb(DB_PATH, SWARM_REPO);
|
|
2764
|
+
var [cmd, ...rawArgs] = process.argv.slice(2);
|
|
2765
|
+
switch (cmd) {
|
|
2766
|
+
case "ls": {
|
|
2767
|
+
const runs = db.listRuns(rawArgs[0]);
|
|
2768
|
+
if (runs.length === 0) console.log("\u0440\u043E\u0439 \u043F\u0443\u0441\u0442");
|
|
2769
|
+
else runs.forEach((r) => console.log(fmtRun(r)));
|
|
2770
|
+
break;
|
|
2771
|
+
}
|
|
2772
|
+
case "spawn": {
|
|
2773
|
+
const { repo: repoFlag, rest: afterRepo } = takeRepo(rawArgs);
|
|
2774
|
+
const { after, rest: afterAfter } = takeAfter(afterRepo);
|
|
2775
|
+
const { hero, rest: afterHero } = takeHero(afterAfter);
|
|
2776
|
+
const { name: nameFlag, rest: args } = takeName(afterHero);
|
|
2777
|
+
const slug = args[0];
|
|
2778
|
+
if (!slug) {
|
|
2779
|
+
console.error("\u043D\u0443\u0436\u0435\u043D slug \u0437\u0430\u0434\u0430\u0447\u0438: swarm spawn PRJ2-27 [area] [--repo <path>] [--after <s1,s2>]");
|
|
2780
|
+
process.exit(1);
|
|
2781
|
+
}
|
|
2782
|
+
const repo = repoFlag ?? SWARM_REPO;
|
|
2783
|
+
if (!repo) {
|
|
2784
|
+
console.error("\u043D\u0435\u0442 \u0440\u0435\u043F\u043E: \u0437\u0430\u0434\u0430\u0439 --repo <path> \u0438\u043B\u0438 SWARM_REPO");
|
|
2785
|
+
process.exit(1);
|
|
2786
|
+
}
|
|
2787
|
+
if (hero) {
|
|
2788
|
+
const r2 = db.createRun({ task_slug: slug, name: nameFlag ?? "HYDRA", repo, kind: "hero", depends_on: after });
|
|
2789
|
+
console.log(`\u0413\u0435\u0440\u043E\u0439 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438: ${fmtRun(r2)}`);
|
|
2790
|
+
if (after.length) console.log(`\u0436\u0434\u0451\u0442 done \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439: ${after.join(", ")}`);
|
|
2791
|
+
console.log("overmind \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u0438\u0442 \u0438 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442 \u0413\u0435\u0440\u043E\u044F (\u043A\u043E\u0433\u0434\u0430 \u0437\u0430\u043F\u0443\u0449\u0435\u043D).");
|
|
2792
|
+
break;
|
|
2793
|
+
}
|
|
2794
|
+
const r = db.createRun({ task_slug: slug, repo, area: args[1] ?? null, depends_on: after });
|
|
2795
|
+
console.log(`\u043F\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u044C: ${fmtRun(r)}`);
|
|
2796
|
+
if (after.length) console.log(`\u0436\u0434\u0451\u0442 done \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439: ${after.join(", ")}`);
|
|
2797
|
+
console.log("overmind \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u0438\u0442 \u0438 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442 \u0434\u0440\u043E\u043D\u0430 (\u043A\u043E\u0433\u0434\u0430 \u0437\u0430\u043F\u0443\u0449\u0435\u043D).");
|
|
2798
|
+
break;
|
|
2799
|
+
}
|
|
2800
|
+
case "wake": {
|
|
2801
|
+
const runId = Number(rawArgs[0]);
|
|
2802
|
+
if (!runId) {
|
|
2803
|
+
console.error("\u043D\u0443\u0436\u0435\u043D runId: swarm wake 42");
|
|
2804
|
+
process.exit(1);
|
|
2805
|
+
}
|
|
2806
|
+
const run = db.getRun(runId);
|
|
2807
|
+
if (!run) {
|
|
2808
|
+
console.error(`\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D`);
|
|
2809
|
+
process.exit(1);
|
|
2810
|
+
}
|
|
2811
|
+
if (run.status === "running") {
|
|
2812
|
+
console.error(`\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0443\u0436\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D (running) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E`);
|
|
2813
|
+
process.exit(1);
|
|
2814
|
+
}
|
|
2815
|
+
const launch = makeLauncher({ overmindEntry: OVERMIND_ENTRY, dbPath: DB_PATH });
|
|
2816
|
+
await respawnRun(
|
|
2817
|
+
{ db, launch, repoFallback: SWARM_REPO, worktreesDir: WORKTREES_DIR, getTask: () => Promise.resolve(null) },
|
|
2818
|
+
run,
|
|
2819
|
+
"continue"
|
|
2820
|
+
);
|
|
2821
|
+
console.log(`\u0440\u0430\u0437\u0431\u0443\u0436\u0435\u043D (--continue): ${fmtRun(db.getRun(runId))}`);
|
|
2822
|
+
break;
|
|
2823
|
+
}
|
|
2824
|
+
case "import-db": {
|
|
2825
|
+
const { repo: repoFlag, rest: args } = takeRepo(rawArgs);
|
|
2826
|
+
const path = args[0];
|
|
2827
|
+
const repo = repoFlag ?? SWARM_REPO;
|
|
2828
|
+
if (!path || !repo) {
|
|
2829
|
+
console.error("\u043D\u0443\u0436\u043D\u043E: swarm import-db <path> --repo <path>");
|
|
2830
|
+
process.exit(1);
|
|
2831
|
+
}
|
|
2832
|
+
const res = db.importDb(path, repo);
|
|
2833
|
+
if (res.imported) console.log(`\u0438\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043E ${res.runs} \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432 \u0438\u0437 ${path} (repo=${repo})`);
|
|
2834
|
+
else console.log(`${path} \u0443\u0436\u0435 \u0438\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D \u0440\u0430\u043D\u0435\u0435 (${res.runs} \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432) \u2014 no-op`);
|
|
2835
|
+
break;
|
|
2836
|
+
}
|
|
2837
|
+
case "questions": {
|
|
2838
|
+
const qs = db.openQuestions();
|
|
2839
|
+
if (qs.length === 0) console.log("\u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u043D\u0435\u0442");
|
|
2840
|
+
else
|
|
2841
|
+
qs.forEach((q) => {
|
|
2842
|
+
const run = db.getRun(q.run_id);
|
|
2843
|
+
console.log(`Q#${q.id} [${run?.task_slug ?? "?"}] ${q.text}`);
|
|
2844
|
+
});
|
|
2845
|
+
break;
|
|
2846
|
+
}
|
|
2847
|
+
case "answer": {
|
|
2848
|
+
const qid = Number(rawArgs[0]);
|
|
2849
|
+
const text = rawArgs.slice(1).join(" ");
|
|
2850
|
+
if (!qid || !text) {
|
|
2851
|
+
console.error("\u043D\u0443\u0436\u043D\u043E: swarm answer <qid> <\u043E\u0442\u0432\u0435\u0442>");
|
|
2852
|
+
process.exit(1);
|
|
2853
|
+
}
|
|
2854
|
+
const q = db.answer(qid, text, "orchestrator");
|
|
2855
|
+
if (!q) console.error(`\u0432\u043E\u043F\u0440\u043E\u0441 #${qid} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D`);
|
|
2856
|
+
else if (q.status !== "answered") console.error(`\u0432\u043E\u043F\u0440\u043E\u0441 #${qid} \u0443\u0436\u0435 \u0437\u0430\u043A\u0440\u044B\u0442`);
|
|
2857
|
+
else console.log(`\u043E\u0442\u0432\u0435\u0442 \u0437\u0430\u043F\u0438\u0441\u0430\u043D, \u0434\u0440\u043E\u043D #${q.run_id} \u0440\u0430\u0437\u0431\u0443\u0436\u0435\u043D (\u043A\u043E\u0433\u0434\u0430 overmind \u0440\u0443\u043B\u0438\u0442)`);
|
|
2858
|
+
break;
|
|
2859
|
+
}
|
|
2860
|
+
case "cancel": {
|
|
2861
|
+
const slug = rawArgs[0];
|
|
2862
|
+
const reason = rawArgs.slice(1).join(" ").trim();
|
|
2863
|
+
if (!slug || !reason) {
|
|
2864
|
+
console.error("\u043D\u0443\u0436\u043D\u043E: swarm cancel <slug> <\u043F\u0440\u0438\u0447\u0438\u043D\u0430>");
|
|
2865
|
+
process.exit(1);
|
|
2866
|
+
}
|
|
2867
|
+
const stopped = await cancelRun(
|
|
2868
|
+
{
|
|
2869
|
+
db,
|
|
2870
|
+
kill: (pid) => process.kill(pid, "SIGTERM"),
|
|
2871
|
+
removeWorktree: (repo, p) => {
|
|
2872
|
+
const r = repo || SWARM_REPO;
|
|
2873
|
+
return r ? removeWorktree(r, p) : Promise.resolve();
|
|
2874
|
+
}
|
|
2875
|
+
},
|
|
2876
|
+
slug,
|
|
2877
|
+
reason
|
|
2878
|
+
);
|
|
2879
|
+
if (!stopped) console.error(`\u043D\u0435\u0442 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043F\u043E slug ${slug}`);
|
|
2880
|
+
else console.log(`\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D: ${fmtRun(stopped)}`);
|
|
2881
|
+
break;
|
|
2882
|
+
}
|
|
2883
|
+
case "log": {
|
|
2884
|
+
const runId = Number(rawArgs[0]);
|
|
2885
|
+
if (!runId) {
|
|
2886
|
+
console.error("\u043D\u0443\u0436\u0435\u043D runId: swarm log 1");
|
|
2887
|
+
process.exit(1);
|
|
2888
|
+
}
|
|
2889
|
+
const run = db.getRun(runId);
|
|
2890
|
+
if (run) console.log(`#${run.id} ${run.name} ${run.task_slug}${run.repo ? ` (${basename2(run.repo)})` : ""}`);
|
|
2891
|
+
const events = db.events(runId);
|
|
2892
|
+
if (events.length === 0) console.log("\u0441\u043E\u0431\u044B\u0442\u0438\u0439 \u043D\u0435\u0442");
|
|
2893
|
+
else events.forEach((e) => console.log(`${e.at} ${e.kind}${e.text ? " " + e.text : ""}`));
|
|
2894
|
+
break;
|
|
2895
|
+
}
|
|
2896
|
+
case "login": {
|
|
2897
|
+
const { workspace, rest } = takeWorkspace(rawArgs);
|
|
2898
|
+
const noOpen = rest.includes("--no-open");
|
|
2899
|
+
const code = await runLogin(db, { workspace, noOpen });
|
|
2900
|
+
db.close();
|
|
2901
|
+
process.exit(code);
|
|
2902
|
+
}
|
|
2903
|
+
case "logout": {
|
|
2904
|
+
clearStore();
|
|
2905
|
+
clearWorkspace();
|
|
2906
|
+
console.log("\u0432\u044B\u0448\u0435\u043B: \u0443\u0434\u0430\u043B\u0435\u043D\u044B oauth.json \u0438 workspace.json (DCR-\u043A\u043B\u0438\u0435\u043D\u0442 \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D \u0434\u043B\u044F \u0441\u043B\u0435\u0434. \u0432\u0445\u043E\u0434\u0430).");
|
|
2907
|
+
break;
|
|
2908
|
+
}
|
|
2909
|
+
case "whoami": {
|
|
2910
|
+
const store = loadStore();
|
|
2911
|
+
if (!store) {
|
|
2912
|
+
console.log("\u043D\u0435 \u0432 \u0441\u0435\u0441\u0441\u0438\u0438. \u0412\u043E\u0439\u0442\u0438: swarm login");
|
|
2913
|
+
break;
|
|
2914
|
+
}
|
|
2915
|
+
const ws = loadWorkspace();
|
|
2916
|
+
console.log(`\u0432\u043E\u0448\u0451\u043B \u043A\u0430\u043A ${store.email ?? "?"}`);
|
|
2917
|
+
console.log(
|
|
2918
|
+
ws ? `\u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441: ${ws.name ?? ws.slug ?? ws.id} (${ws.slug ?? ws.id})` : "\u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441: \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D (\u0441\u0435\u0440\u0432\u0435\u0440 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442 \u0444\u043E\u043B\u0431\u044D\u043A) \u2014 \u0432\u044B\u0431\u0435\u0440\u0438: swarm login"
|
|
2919
|
+
);
|
|
2920
|
+
break;
|
|
2921
|
+
}
|
|
2922
|
+
case "web": {
|
|
2923
|
+
const web = startWeb({ db });
|
|
2924
|
+
console.log(`\u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0430: http://127.0.0.1:${web.port} \u2190 \u043E\u0442\u043A\u0440\u043E\u0439 \u0438 \u0432\u043E\u0439\u0434\u0438 \u0447\u0435\u0440\u0435\u0437 Taskless`);
|
|
2925
|
+
await new Promise(() => {
|
|
2926
|
+
});
|
|
2927
|
+
break;
|
|
2928
|
+
}
|
|
2929
|
+
default:
|
|
2930
|
+
usage();
|
|
2931
|
+
}
|
|
2932
|
+
db.close();
|