oficina-opencode 3.1.0 → 3.1.1

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.
Files changed (69) hide show
  1. package/README.md +1 -1
  2. package/commands/oficina.md +7 -6
  3. package/dashboard/.dom-check.html +1257 -0
  4. package/dashboard/README-DASHBOARD.md +64 -0
  5. package/dashboard/assets/factory.css +1543 -0
  6. package/dashboard/assets/tokens.css +208 -0
  7. package/dashboard/build-kanban-data.mjs +279 -0
  8. package/dashboard/dashboard.css +753 -0
  9. package/dashboard/dashboard.html +121 -0
  10. package/dashboard/dashboard.js +610 -0
  11. package/dashboard/data/snapshot.json +883 -0
  12. package/dashboard/debate.html +134 -0
  13. package/dashboard/factory/build-snapshot.mjs +89 -0
  14. package/dashboard/factory/factory-app.js +1185 -0
  15. package/dashboard/factory/factory-core.js +541 -0
  16. package/dashboard/factory/snapshot.js +850 -0
  17. package/dashboard/factory.html +350 -0
  18. package/dashboard/kanban-data.json +671 -0
  19. package/dashboard/kanban-source.js +643 -0
  20. package/dashboard/kanban.check.mjs +454 -0
  21. package/dashboard/kanban.html +1257 -0
  22. package/dashboard/leadership.html +1008 -0
  23. package/dashboard/lib/debate-app.js +1149 -0
  24. package/dashboard/lib/debate-core.js +698 -0
  25. package/dashboard/lib/debate-data.js +163 -0
  26. package/dashboard/lib/debate-demo.js +110 -0
  27. package/dashboard/lib/debate.css +1155 -0
  28. package/dashboard/lib/leadership-model.js +946 -0
  29. package/dashboard/probe.mjs +35 -0
  30. package/dashboard/scripts/serve.mjs +122 -0
  31. package/dashboard/scripts/snapshot-data.mjs +93 -0
  32. package/dashboard/scripts/snapshot.mjs +28 -0
  33. package/dashboard/server.mjs +242 -0
  34. package/dashboard/shot.mjs +54 -0
  35. package/dashboard/shots/desktop-gates.png +0 -0
  36. package/dashboard/shots/desktop-light.png +0 -0
  37. package/dashboard/shots/desktop-real.png +0 -0
  38. package/dashboard/shots/mobile.png +0 -0
  39. package/dashboard/test/factory-core.test.mjs +244 -0
  40. package/dashboard/tests/debate-core.test.mjs +336 -0
  41. package/dashboard/tests/leadership-model.test.mjs +404 -0
  42. package/dist/cli.js +24 -0
  43. package/dist/cli.js.map +1 -1
  44. package/dist/core/dashboard.d.ts +33 -0
  45. package/dist/core/dashboard.d.ts.map +1 -0
  46. package/dist/core/dashboard.js +115 -0
  47. package/dist/core/dashboard.js.map +1 -0
  48. package/dist/core/dispatch.d.ts.map +1 -1
  49. package/dist/core/dispatch.js +11 -0
  50. package/dist/core/dispatch.js.map +1 -1
  51. package/dist/core/environment.d.ts +28 -0
  52. package/dist/core/environment.d.ts.map +1 -0
  53. package/dist/core/environment.js +80 -0
  54. package/dist/core/environment.js.map +1 -0
  55. package/dist/core/systemPrompt.d.ts.map +1 -1
  56. package/dist/core/systemPrompt.js +3 -0
  57. package/dist/core/systemPrompt.js.map +1 -1
  58. package/dist/tools/index.d.ts +9 -1
  59. package/dist/tools/index.d.ts.map +1 -1
  60. package/dist/tools/index.js +26 -22
  61. package/dist/tools/index.js.bak +972 -0
  62. package/dist/tools/index.js.map +1 -1
  63. package/dist/v2.d.ts +5 -0
  64. package/dist/v2.d.ts.map +1 -1
  65. package/dist/v2.js +57 -5
  66. package/dist/v2.js.bak +517 -0
  67. package/dist/v2.js.map +1 -1
  68. package/docs/QUE-ES.md +1 -1
  69. package/package.json +4 -1
@@ -0,0 +1,1185 @@
1
+ /* ==========================================================================
2
+ dashboard/factory.html — app layer (classic script, no build step)
3
+ Depends on: globalThis.FactoryCore (dashboard/factory/factory-core.js)
4
+ Data: live -> /api/snapshot (dashboard/serve.mjs)
5
+ static-> ../.oficina/*.json (any static server at repo root)
6
+ offline-> window.__FACTORY_SNAPSHOT__ (dashboard/factory/snapshot.js)
7
+ The panel is strictly read-only: "registering" an agent is a local
8
+ projection so the animation can be watched without faking a disk write.
9
+ ========================================================================== */
10
+
11
+ ;(function factoryApp() {
12
+ "use strict"
13
+
14
+ var Core = globalThis.FactoryCore
15
+ var doc = document
16
+ var root = doc.documentElement
17
+
18
+ if (!Core) {
19
+ doc.body.insertAdjacentHTML(
20
+ "afterbegin",
21
+ '<p style="padding:2rem;font-family:monospace;color:#f88">' +
22
+ "factory-core.js no cargó: revisá la ruta del script.</p>",
23
+ )
24
+ return
25
+ }
26
+
27
+ // ---------------------------------------------------------------- helpers
28
+ var $ = function (sel, ctx) {
29
+ return (ctx || doc).querySelector(sel)
30
+ }
31
+ var $$ = function (sel, ctx) {
32
+ return Array.prototype.slice.call((ctx || doc).querySelectorAll(sel))
33
+ }
34
+ var sleep = function (ms) {
35
+ return new Promise(function (resolve) {
36
+ setTimeout(resolve, ms)
37
+ })
38
+ }
39
+ var esc = function (value) {
40
+ return String(value == null ? "" : value)
41
+ .replace(/&/g, "&amp;")
42
+ .replace(/</g, "&lt;")
43
+ .replace(/>/g, "&gt;")
44
+ .replace(/"/g, "&quot;")
45
+ .replace(/'/g, "&#39;")
46
+ }
47
+ var reducedMotion = window.matchMedia(
48
+ "(prefers-reduced-motion: reduce)",
49
+ ).matches
50
+ var clamp = function (n, min, max) {
51
+ return Math.max(min, Math.min(max, n))
52
+ }
53
+ var initials = function (name) {
54
+ var words = String(name || "?")
55
+ .replace(/[^A-Za-z0-9áéíóúñ ]/gi, " ")
56
+ .split(/\s+/)
57
+ .filter(Boolean)
58
+ if (words.length === 0) return "??"
59
+ if (words.length === 1) return words[0].slice(0, 2).toUpperCase()
60
+ return (words[0][0] + words[words.length - 1][0]).toUpperCase()
61
+ }
62
+ var timeOf = function (iso) {
63
+ var date = iso ? new Date(iso) : new Date()
64
+ if (isNaN(date.getTime())) return "--:--:--"
65
+ return date.toLocaleTimeString("es-AR", { hour12: false })
66
+ }
67
+ var STORAGE_THEME = "oficina.factory.theme"
68
+
69
+ // ---------------------------------------------------------------- elements
70
+ var els = {
71
+ sourceChip: $("#source-chip"),
72
+ sourceText: $("#source-text"),
73
+ pollChip: $("#poll-chip"),
74
+ clockChip: $("#clock-chip"),
75
+ countsChip: $("#counts-chip"),
76
+ btnFabricate: $("#btn-fabricate"),
77
+ btnPause: $("#btn-pause"),
78
+ btnResync: $("#btn-resync"),
79
+ btnTheme: $("#btn-theme"),
80
+ fabricateHint: $("#fabricate-hint"),
81
+ coverageValue: $("#coverage-value"),
82
+ coverageFill: $("#coverage-fill"),
83
+ coverageThreshold: $("#coverage-threshold"),
84
+ coverageNote: $("#coverage-note"),
85
+ gapList: $("#gap-list"),
86
+ gapCount: $("#gap-count"),
87
+ stage: $("#stage"),
88
+ sceneMoldLabel: $("#scene-mold-label"),
89
+ sceneAgent: $("#scene-agent"),
90
+ sceneOutLabel: $("#scene-out-label"),
91
+ steps: $$("#steps .step"),
92
+ termPath: $("#term-path"),
93
+ termPathText: $("#term-path-text"),
94
+ termTag: $("#term-tag"),
95
+ termBody: $("#term-body"),
96
+ rosterGrid: $("#roster-grid"),
97
+ rosterCount: $("#roster-count"),
98
+ matchBody: $("#match-body"),
99
+ matchCount: $("#match-count"),
100
+ toasts: $("#toasts"),
101
+ dialog: $("#agent-dialog"),
102
+ dialogAvatar: $("#dialog-avatar"),
103
+ dialogTitle: $("#dialog-title"),
104
+ dialogSub: $("#dialog-sub"),
105
+ dialogMeta: $("#dialog-meta"),
106
+ dialogBody: $("#dialog-body"),
107
+ dialogOrigin: $("#dialog-origin"),
108
+ btnCopy: $("#btn-copy"),
109
+ live: $("#live-region"),
110
+ }
111
+
112
+ // ---------------------------------------------------------------- state
113
+ var app = {
114
+ snapshot: null,
115
+ sourceKind: "none",
116
+ need: null,
117
+ focusId: null,
118
+ preview: null,
119
+ fabricated: null,
120
+ projection: null,
121
+ running: false,
122
+ runToken: 0,
123
+ paused: false,
124
+ tick: 0,
125
+ interval: 800,
126
+ unchanged: 0,
127
+ lastSignature: "",
128
+ startedAt: null,
129
+ confirmed: false,
130
+ fichaOrigin: "proyectada",
131
+ pendingAuto: false,
132
+ }
133
+
134
+ var STEPS = [
135
+ { id: "detect", label: "Detectar hueco de cobertura", end: "GAP" },
136
+ { id: "synthesize", label: "Sintetizar rol y capabilities", end: "ROL" },
137
+ { id: "write", label: "Escribir la ficha .md", end: "FICHA" },
138
+ { id: "register", label: "Registrar en el catálogo dinámico", end: "ROSTER" },
139
+ { id: "match", label: "Match en la próxima card", end: "MATCH" },
140
+ ]
141
+
142
+ // ---------------------------------------------------------------- announce
143
+ function announce(message) {
144
+ if (!els.live) return
145
+ els.live.textContent = ""
146
+ // force SR re-announcement for identical strings
147
+ setTimeout(function () {
148
+ els.live.textContent = message
149
+ }, 30)
150
+ }
151
+
152
+ // ---------------------------------------------------------------- data
153
+ async function fetchJson(url) {
154
+ var response = await fetch(url, { cache: "no-store" })
155
+ if (!response.ok) throw new Error(url + " -> " + response.status)
156
+ return response.json()
157
+ }
158
+
159
+ async function loadFromApi() {
160
+ var data = await fetchJson("/api/snapshot")
161
+ if (!data || !data.state || !data.roster) throw new Error("payload inválido")
162
+ return {
163
+ source: "live",
164
+ capturedAt: data.capturedAt,
165
+ state: data.state,
166
+ roster: data.roster,
167
+ catalogo: data.catalogo || [],
168
+ fichas: data.fichas || {},
169
+ }
170
+ }
171
+
172
+ async function loadFromFiles() {
173
+ var pair = await Promise.all([
174
+ fetchJson("../.oficina/state.json"),
175
+ fetchJson("../.oficina/roster.json"),
176
+ ])
177
+ return {
178
+ source: "static",
179
+ capturedAt: new Date().toISOString(),
180
+ state: pair[0],
181
+ roster: pair[1],
182
+ catalogo: [],
183
+ fichas: {},
184
+ }
185
+ }
186
+
187
+ function loadFromEmbedded() {
188
+ var snap = window.__FACTORY_SNAPSHOT__
189
+ if (!snap || !snap.state || !snap.roster) throw new Error("sin snapshot embebido")
190
+ return {
191
+ source: "snapshot",
192
+ capturedAt: snap.capturedAt,
193
+ state: snap.state,
194
+ roster: snap.roster,
195
+ catalogo: snap.catalogo || [],
196
+ fichas: snap.fichas || {},
197
+ }
198
+ }
199
+
200
+ async function acquireData() {
201
+ var errors = []
202
+ var loaders = [
203
+ ["live", loadFromApi],
204
+ ["static", loadFromFiles],
205
+ ["snapshot", loadFromEmbedded],
206
+ ]
207
+ for (var i = 0; i < loaders.length; i += 1) {
208
+ try {
209
+ return await loaders[i][1]()
210
+ } catch (error) {
211
+ errors.push(loaders[i][0] + ": " + error.message)
212
+ }
213
+ }
214
+ throw new Error("sin fuente de datos — " + errors.join(" · "))
215
+ }
216
+
217
+ function signatureOf(data) {
218
+ var rosterUpdated = (data.roster && data.roster.updatedAt) || ""
219
+ var stateUpdated = (data.state && data.state.updatedAt) || ""
220
+ var cards = Object.keys((data.state && data.state.cards) || {}).length
221
+ var agents = Object.keys((data.roster && data.roster.agents) || {}).length
222
+ return [rosterUpdated, stateUpdated, cards, agents].join("|")
223
+ }
224
+
225
+ /**
226
+ * Merge the local projection into the real roster so the UI can show the
227
+ * fabricated agent without pretending roster.json changed on disk.
228
+ */
229
+ function effectiveRoster() {
230
+ if (!app.snapshot) return { agents: {} }
231
+ if (!app.fabricated) return app.snapshot.roster
232
+ return Core.registerAgent(app.snapshot.roster, app.fabricated, {
233
+ now: app.fabricated.fabricatedAt,
234
+ cardTitle: cardById(app.fabricated.fabricatedFor)
235
+ ? cardById(app.fabricated.fabricatedFor).title
236
+ : null,
237
+ })
238
+ }
239
+
240
+ function cardById(id) {
241
+ if (!app.snapshot) return null
242
+ return (
243
+ Core.parseCards(app.snapshot.state).filter(function (card) {
244
+ return card.id === id
245
+ })[0] || null
246
+ )
247
+ }
248
+
249
+ // ---------------------------------------------------------------- sync
250
+ async function sync(options) {
251
+ var opts = options || {}
252
+ if (app.running) return
253
+ try {
254
+ var data = await acquireData()
255
+ var signature = signatureOf(data)
256
+ var changed = signature !== app.lastSignature
257
+ app.snapshot = data
258
+ app.sourceKind = data.source
259
+ app.lastSignature = signature
260
+ app.startedAt = data.capturedAt || new Date().toISOString()
261
+
262
+ // Confirm a projection that actually landed in roster.json.
263
+ if (
264
+ app.fabricated &&
265
+ data.roster &&
266
+ data.roster.agents &&
267
+ data.roster.agents[app.fabricated.id]
268
+ ) {
269
+ app.confirmed = true
270
+ }
271
+
272
+ recompute()
273
+ renderAll()
274
+ setSourceChip(data.source)
275
+ els.clockChip.textContent = "sync " + timeOf(app.startedAt)
276
+ els.clockChip.title = "última lectura: " + app.startedAt
277
+
278
+ if (changed) {
279
+ app.unchanged = 0
280
+ app.interval = 800
281
+ } else {
282
+ app.unchanged += 1
283
+ if (app.unchanged > 8) app.interval = clamp(Math.round(app.interval * 1.5), 800, 4000)
284
+ }
285
+ els.pollChip.textContent = "poll " + app.interval + "ms"
286
+ if (opts.manual) announce("Datos sincronizados desde " + data.source + ".")
287
+ } catch (error) {
288
+ app.sourceKind = "error"
289
+ setSourceChip("error")
290
+ els.clockChip.textContent = "sin datos"
291
+ if (opts.manual) announce("No se pudo leer .oficina/: " + error.message)
292
+ console.error("[factory]", error)
293
+ }
294
+ }
295
+
296
+ function setSourceChip(kind) {
297
+ var labels = {
298
+ live: "LIVE · api",
299
+ static: "STATIC · http",
300
+ snapshot: "OFFLINE · snapshot",
301
+ error: "SIN DATOS",
302
+ }
303
+ els.sourceChip.setAttribute("data-live", kind)
304
+ els.sourceText.textContent = labels[kind] || kind
305
+ els.sourceChip.title =
306
+ kind === "live"
307
+ ? "leyendo .oficina/ vía dashboard/serve.mjs"
308
+ : kind === "static"
309
+ ? "leyendo ../.oficina/*.json por HTTP"
310
+ : kind === "snapshot"
311
+ ? "snapshot embebido: regeneralo con node dashboard/factory/build-snapshot.mjs"
312
+ : "no se pudo leer .oficina/"
313
+ }
314
+
315
+ // ---------------------------------------------------------------- compute
316
+ function recompute() {
317
+ if (!app.snapshot) return
318
+ var roster = effectiveRoster()
319
+ app.need = Core.detectFactoryNeed(app.snapshot.state, roster, {
320
+ threshold: Core.COVERAGE_THRESHOLD,
321
+ })
322
+ app.preview = buildPreview()
323
+ }
324
+
325
+ function focusGap() {
326
+ if (!app.need) return null
327
+ var gaps = app.need.gaps
328
+ var chosen = gaps.filter(function (gap) {
329
+ return gap.cardId === app.focusId
330
+ })[0]
331
+ if (chosen) return chosen
332
+ var working = gaps.filter(function (gap) {
333
+ return gap.state === "in_progress"
334
+ })[0]
335
+ return working || gaps[0] || null
336
+ }
337
+
338
+ function buildPreview() {
339
+ var gap = focusGap()
340
+ if (!gap) return null
341
+ var card = cardById(gap.cardId)
342
+ if (!card) return null
343
+ var agent = Core.synthesizeAgent(card, app.need.agents)
344
+ var projection = Core.projectMatch(app.snapshot.state, effectiveRoster(), {
345
+ fabricated: agent,
346
+ threshold: Core.COVERAGE_THRESHOLD,
347
+ })
348
+ var self = projection.rows.filter(function (row) {
349
+ return row.cardId === card.id
350
+ })[0]
351
+ var next = projection.nextMatch
352
+ return {
353
+ card: card,
354
+ agent: agent,
355
+ row: self,
356
+ next: next,
357
+ projection: projection,
358
+ }
359
+ }
360
+
361
+ // ---------------------------------------------------------------- render
362
+ function renderAll() {
363
+ renderDetection()
364
+ renderRoster()
365
+ renderMatch()
366
+ renderHero()
367
+ }
368
+
369
+ function renderHero() {
370
+ var preview = app.preview
371
+ var gaps = app.need ? app.need.gaps.length : 0
372
+ els.countsChip.textContent =
373
+ (app.need ? app.need.cardCount : 0) +
374
+ " cards · " +
375
+ (app.need ? app.need.agentCount : 0) +
376
+ " agentes"
377
+
378
+ if (app.running) return
379
+
380
+ if (!preview) {
381
+ els.btnFabricate.disabled = true
382
+ els.btnFabricate.classList.remove("btn-armed")
383
+ els.btnFabricate.querySelector(".btn-label").textContent =
384
+ "Catálogo cubre el plan"
385
+ els.fabricateHint.textContent =
386
+ "Ninguna card abierta cae por debajo del umbral."
387
+ els.sceneMoldLabel.textContent = "—"
388
+ els.sceneAgent.textContent = "sin huecos"
389
+ els.sceneOutLabel.textContent = "—"
390
+ return
391
+ }
392
+
393
+ els.btnFabricate.disabled = false
394
+ els.btnFabricate.classList.add("btn-armed")
395
+ els.btnFabricate.querySelector(".btn-label").textContent =
396
+ gaps > 1 ? "Fabricar agente (" + gaps + " huecos)" : "Fabricar agente"
397
+ els.fabricateHint.textContent =
398
+ preview.agent.displayName + " → " + preview.card.id + " · " + preview.card.title
399
+ els.sceneMoldLabel.textContent = preview.card.id
400
+ els.sceneAgent.textContent = app.fabricated
401
+ ? app.fabricated.displayName
402
+ : preview.agent.displayName
403
+ els.sceneOutLabel.textContent = app.fabricated
404
+ ? app.fabricated.id.replace(/^e-factory-/, "")
405
+ : preview.agent.role
406
+ }
407
+
408
+ function renderDetection() {
409
+ if (!app.need) return
410
+ var need = app.need
411
+ var pct = need.overallPct
412
+ els.coverageValue.innerHTML = pct + "<small>%</small>"
413
+ els.coverageFill.style.width = clamp(pct, 0, 100) + "%"
414
+ els.coverageFill.setAttribute(
415
+ "data-tone",
416
+ pct >= need.thresholdPct ? "ok" : "bad",
417
+ )
418
+ els.coverageFill.parentElement.setAttribute("aria-valuenow", String(pct))
419
+ els.coverageThreshold.style.left = need.thresholdPct + "%"
420
+ els.coverageThreshold.setAttribute(
421
+ "data-label",
422
+ "umbral " + need.thresholdPct + "%",
423
+ )
424
+ els.gapCount.textContent = need.gaps.length + " gaps"
425
+ els.coverageNote.textContent = app.fabricated
426
+ ? "incluye 1 agente fabricado (proyección local)"
427
+ : need.agentCount + " agentes en catálogo · " + need.cardCount + " cards"
428
+
429
+ var gap = focusGap()
430
+ if (gap && !app.focusId) app.focusId = gap.cardId
431
+
432
+ if (need.gaps.length === 0) {
433
+ els.gapList.innerHTML =
434
+ '<div class="empty"><strong>Catálogo completo</strong>' +
435
+ "Todas las cards abiertas superan el " +
436
+ need.thresholdPct +
437
+ "% de cobertura. La factory queda en espera.</div>"
438
+ return
439
+ }
440
+
441
+ var html = need.gaps
442
+ .map(function (item) {
443
+ var missing = item.missing
444
+ var chips = missing
445
+ .slice(0, 4)
446
+ .map(function (cap) {
447
+ return '<span class="cap" data-kind="missing">' + esc(cap) + "</span>"
448
+ })
449
+ .join("")
450
+ if (missing.length > 4) {
451
+ chips += '<span class="cap">+' + (missing.length - 4) + "</span>"
452
+ }
453
+ if (missing.length === 0) {
454
+ chips = '<span class="cap">sin requisitos</span>'
455
+ }
456
+ return (
457
+ '<button type="button" class="gap-card" data-card="' +
458
+ esc(item.cardId) +
459
+ '" aria-pressed="' +
460
+ (app.focusId === item.cardId ? "true" : "false") +
461
+ '" aria-label="Card ' +
462
+ esc(item.cardId) +
463
+ ", cobertura " +
464
+ item.pct +
465
+ "%. " +
466
+ esc(item.title) +
467
+ '">' +
468
+ '<span class="gap-top">' +
469
+ '<span class="gap-id">' +
470
+ esc(item.cardId) +
471
+ "</span>" +
472
+ '<span class="badge" data-state="' +
473
+ esc(item.state) +
474
+ '">' +
475
+ esc(item.state) +
476
+ "</span>" +
477
+ (item.owner ? '<span class="cap">' + esc(item.owner) + "</span>" : "") +
478
+ '<span class="gap-pct" data-tone="' +
479
+ (item.pct >= need.thresholdPct ? "ok" : "bad") +
480
+ '">' +
481
+ item.pct +
482
+ "%</span></span>" +
483
+ '<span class="gap-title">' +
484
+ esc(item.title) +
485
+ "</span>" +
486
+ '<span class="gap-missing">' +
487
+ chips +
488
+ "</span>" +
489
+ "</button>"
490
+ )
491
+ })
492
+ .join("")
493
+ els.gapList.innerHTML = html
494
+
495
+ $$(".gap-card", els.gapList).forEach(function (button) {
496
+ button.addEventListener("click", function () {
497
+ if (app.running) return
498
+ selectCard(button.getAttribute("data-card"))
499
+ })
500
+ })
501
+ }
502
+
503
+ function selectCard(cardId) {
504
+ if (!cardId || cardId === app.focusId) return
505
+ app.focusId = cardId
506
+ resetSequence()
507
+ recompute()
508
+ renderDetection()
509
+ renderMatch()
510
+ renderHero()
511
+ announce("Card " + cardId + " seleccionada como objetivo de fabricación.")
512
+ }
513
+
514
+ function renderRoster(options) {
515
+ var opts = options || {}
516
+ var agents = Core.parseAgents(effectiveRoster())
517
+ agents.sort(function (a, b) {
518
+ var fabricatedA = a.source && a.source.kind === "factory" ? 0 : 1
519
+ var fabricatedB = b.source && b.source.kind === "factory" ? 0 : 1
520
+ if (fabricatedA !== fabricatedB) return fabricatedA - fabricatedB
521
+ if (a.state !== b.state) return a.state === "working" ? -1 : 1
522
+ return a.displayName.localeCompare(b.displayName)
523
+ })
524
+
525
+ els.rosterCount.textContent =
526
+ agents.length + (app.fabricated ? " · proyección" : "")
527
+ els.rosterGrid.innerHTML = agents
528
+ .map(function (agent) {
529
+ var fabricated = agent.source && agent.source.kind === "factory"
530
+ return (
531
+ '<button type="button" class="agent-chip" data-agent="' +
532
+ esc(agent.id) +
533
+ '" data-fabricated="' +
534
+ (fabricated ? "true" : "false") +
535
+ (opts.newId === agent.id ? '" data-new="true' : "") +
536
+ '" aria-label="Abrir ficha de ' +
537
+ esc(agent.displayName) +
538
+ ", " +
539
+ esc(agent.role) +
540
+ '">' +
541
+ '<span class="agent-avatar" aria-hidden="true">' +
542
+ esc(initials(agent.displayName)) +
543
+ "</span>" +
544
+ '<span class="agent-main"><span class="agent-name">' +
545
+ esc(agent.displayName) +
546
+ '</span><span class="agent-role">' +
547
+ esc(agent.role) +
548
+ "</span></span>" +
549
+ '<span class="agent-state" data-state="' +
550
+ esc(agent.state) +
551
+ '"><i></i>' +
552
+ esc(agent.state.replace(/_/g, " ")) +
553
+ "</span>" +
554
+ "</button>"
555
+ )
556
+ })
557
+ .join("")
558
+
559
+ $$(".agent-chip[data-new]", els.rosterGrid).forEach(function (chip) {
560
+ chip.classList.add("is-new")
561
+ })
562
+
563
+ $$(".agent-chip", els.rosterGrid).forEach(function (chip) {
564
+ chip.addEventListener("click", function () {
565
+ var agent = agents.filter(function (item) {
566
+ return item.id === chip.getAttribute("data-agent")
567
+ })[0]
568
+ if (agent) openAgent(agent)
569
+ })
570
+ })
571
+ }
572
+
573
+ function renderMatch(options) {
574
+ var opts = options || {}
575
+ if (!app.preview) {
576
+ els.matchBody.innerHTML =
577
+ '<div class="empty">Sin proyección: el catálogo ya cubre las cards abiertas.</div>'
578
+ els.matchCount.textContent = "0"
579
+ return
580
+ }
581
+ var preview = app.preview
582
+ var rows = []
583
+ rows.push({ row: preview.row, label: "dispara", card: preview.card })
584
+ if (preview.next && preview.next.cardId !== preview.card.id) {
585
+ rows.push({ row: preview.next, label: "próxima card", card: null })
586
+ }
587
+
588
+ els.matchCount.textContent = rows.length + " cards"
589
+ els.matchBody.innerHTML = rows
590
+ .map(function (entry, index) {
591
+ var row = entry.row
592
+ var revealed = opts.revealed || app.fabricated
593
+ var afterPct = revealed ? row.after.pct : row.before.pct
594
+ var matched = revealed && row.after.covered
595
+ return (
596
+ '<div class="match-card" data-matched="' +
597
+ (matched ? "true" : "false") +
598
+ '">' +
599
+ '<span class="match-stamp">MATCH</span>' +
600
+ '<span class="match-id">' +
601
+ esc(row.cardId) +
602
+ '</span><span class="match-name">' +
603
+ esc(entry.card ? entry.card.title : row.title) +
604
+ "</span>" +
605
+ '<div class="match-bars"><span>' +
606
+ (index === 0 ? "actual" : "match") +
607
+ '</span><div class="meter"><span class="meter-fill" data-tone="' +
608
+ (row.before.covered ? "ok" : "bad") +
609
+ '" style="width:' +
610
+ row.before.pct +
611
+ '%"></span></div><span class="match-value">' +
612
+ row.before.pct +
613
+ "%</span></div>" +
614
+ '<div class="match-bars"><span>post</span><div class="meter">' +
615
+ '<span class="meter-fill" data-tone="' +
616
+ (row.after.covered ? "ok" : "bad") +
617
+ '" style="width:' +
618
+ afterPct +
619
+ '%"></span></div><span class="match-value">' +
620
+ afterPct +
621
+ "%</span></div>" +
622
+ "</div>"
623
+ )
624
+ })
625
+ .join("")
626
+ }
627
+
628
+ // ---------------------------------------------------------------- steps
629
+ function setStep(index, status, meta) {
630
+ var step = els.steps[index]
631
+ if (!step) return
632
+ step.setAttribute("data-status", status)
633
+ if (status === "active") step.setAttribute("aria-current", "step")
634
+ else step.removeAttribute("aria-current")
635
+ var icon = $(".step-icon", step)
636
+ if (icon) {
637
+ if (status === "done") icon.textContent = "✓"
638
+ else if (status === "active") icon.textContent = "◆"
639
+ else if (status === "skipped") icon.textContent = "–"
640
+ else icon.textContent = String(index + 1).padStart(2, "0")
641
+ }
642
+ var end = $(".step-end", step)
643
+ if (end) {
644
+ end.textContent =
645
+ status === "done"
646
+ ? "ok"
647
+ : status === "active"
648
+ ? "run"
649
+ : status === "skipped"
650
+ ? "skip"
651
+ : STEPS[index].end
652
+ }
653
+ if (meta != null) {
654
+ var metaEl = $(".step-meta", step)
655
+ if (metaEl) metaEl.textContent = meta
656
+ }
657
+ }
658
+
659
+ function resetSequence() {
660
+ app.runToken += 1
661
+ app.running = false
662
+ app.fabricated = null
663
+ app.projection = null
664
+ app.confirmed = false
665
+ els.steps.forEach(function (_, index) {
666
+ setStep(index, "pending", "")
667
+ })
668
+ setScene("idle")
669
+ els.termPathText.textContent = "sin ficha en fabricación"
670
+ els.termTag.textContent = "idle"
671
+ els.termBody.innerHTML =
672
+ '<span class="term-placeholder">La ficha .md se escribe acá cuando la factory sintetiza un agente.</span>'
673
+ setStep(0, "pending", "esperando disparo")
674
+ }
675
+
676
+ function setScene(name) {
677
+ els.stage.setAttribute("data-scene", name)
678
+ }
679
+
680
+ function kindOf(line) {
681
+ if (/^#{1,2} /.test(line)) return "head"
682
+ if (/^---$/.test(line)) return "front"
683
+ if (/^(description|mode|capabilities):/.test(line)) return "front"
684
+ if (/^#/.test(line)) return "dim"
685
+ return ""
686
+ }
687
+
688
+ function scrollTerm() {
689
+ els.termBody.scrollTop = els.termBody.scrollHeight
690
+ }
691
+
692
+ async function typeFicha(lines, token) {
693
+ els.termBody.innerHTML = ""
694
+ var cursor = doc.createElement("span")
695
+ cursor.className = "cursor"
696
+ cursor.setAttribute("aria-hidden", "true")
697
+ var total = lines.length
698
+ for (var i = 0; i < lines.length; i += 1) {
699
+ if (token !== app.runToken) return
700
+ var text = lines[i]
701
+ var lineEl = doc.createElement("span")
702
+ lineEl.className = "term-line"
703
+ var kind = kindOf(text)
704
+ if (kind) lineEl.setAttribute("data-kind", kind)
705
+ lineEl.textContent = text || "\u00a0"
706
+ els.termBody.appendChild(lineEl)
707
+ els.termBody.appendChild(cursor)
708
+ scrollTerm()
709
+ setStep(2, "active", "escribiendo " + (i + 1) + "/" + total + " líneas")
710
+ if (!reducedMotion && text.length > 0) {
711
+ var perTick = Math.max(2, Math.ceil(text.length / 60))
712
+ for (var pos = perTick; pos < text.length; pos += perTick) {
713
+ if (token !== app.runToken) return
714
+ lineEl.textContent = text.slice(0, pos)
715
+ scrollTerm()
716
+ await sleep(text.length > 70 ? 5 : 9)
717
+ }
718
+ lineEl.textContent = text
719
+ }
720
+ if (i % 3 === 0) {
721
+ // yield so the UI can paint during long fichas
722
+ await sleep(reducedMotion ? 0 : 12)
723
+ }
724
+ }
725
+ if (token !== app.runToken) return
726
+ setStep(2, "done", total + " líneas · " + app.fabricated.fichaPath)
727
+ els.termTag.textContent = "escrita"
728
+ }
729
+
730
+ // ---------------------------------------------------------------- fabricate
731
+ async function fabricate(cardId) {
732
+ if (app.running || !app.preview) return
733
+ var token = ++app.runToken
734
+ var preview = app.preview
735
+ var card = preview.card
736
+ var agent = Core.synthesizeAgent(card, app.need.agents, {
737
+ now: new Date().toISOString(),
738
+ })
739
+ var projection = Core.projectMatch(app.snapshot.state, effectiveRoster(), {
740
+ fabricated: agent,
741
+ threshold: Core.COVERAGE_THRESHOLD,
742
+ })
743
+ var speed = reducedMotion ? 0.05 : 1
744
+ var wait = function (ms) {
745
+ return sleep(ms * speed)
746
+ }
747
+
748
+ app.running = true
749
+ app.fabricated = agent
750
+ app.projection = projection
751
+ app.confirmed = false
752
+ els.btnFabricate.disabled = true
753
+ els.fabricateHint.textContent = "fabricando " + agent.id + "…"
754
+ els.sceneMoldLabel.textContent = card.id
755
+ els.sceneAgent.textContent = agent.displayName
756
+ els.sceneOutLabel.textContent = agent.role
757
+ els.steps.forEach(function (_, index) {
758
+ setStep(index, "pending", "")
759
+ })
760
+
761
+ // 1 · detection
762
+ setScene("detect")
763
+ setStep(0, "active", "cobertura " + preview.row.before.pct + "% < 60% · " + card.id)
764
+ announce("Factory detectó cobertura insuficiente en " + card.id + ".")
765
+ await wait(620)
766
+ if (token !== app.runToken) return
767
+ setStep(0, "done", card.id + " · " + preview.row.before.pct + "% · faltan " + preview.row.before.missing.length)
768
+
769
+ // 2 · synthesize
770
+ setScene("synthesize")
771
+ setStep(1, "active", "rol: " + agent.role)
772
+ await wait(780)
773
+ if (token !== app.runToken) return
774
+ setStep(1, "done", agent.capabilities.length + " capabilities · " + agent.id)
775
+
776
+ // 3 · write the ficha
777
+ setScene("write")
778
+ els.termPathText.textContent = agent.fichaPath
779
+ els.termTag.textContent = "escribiendo"
780
+ els.termPath.title = agent.fichaPath
781
+ var lines = Core.renderFichaLines(agent, card)
782
+ var dirLine = "# oficina factory --card " + card.id + " --role " + agent.role
783
+ await typeFicha([dirLine].concat(lines), token)
784
+ if (token !== app.runToken) return
785
+
786
+ // 4 · register in the catalog
787
+ setScene("register")
788
+ setStep(3, "active", "roster.json ← " + agent.id)
789
+ renderRoster({ newId: agent.id })
790
+ announce(agent.displayName + " registrado en el catálogo.")
791
+ await wait(760)
792
+ if (token !== app.runToken) return
793
+ setStep(3, "done", (app.need.agentCount + 1) + " agentes en catálogo")
794
+
795
+ // 5 · next card matches
796
+ setScene("match")
797
+ setStep(4, "active", "recalculando cobertura…")
798
+ recompute()
799
+ renderDetection()
800
+ renderMatch({ revealed: true })
801
+ await wait(760)
802
+ if (token !== app.runToken) return
803
+ var next = projection.nextMatch
804
+ setStep(
805
+ 4,
806
+ "done",
807
+ next
808
+ ? next.cardId + " · " + next.before.pct + "% → " + next.after.pct + "%"
809
+ : card.id + " · " + preview.row.before.pct + "% → " + preview.row.after.pct + "%",
810
+ )
811
+
812
+ setScene("done")
813
+ app.running = false
814
+ els.termTag.textContent = "lista"
815
+ renderHero()
816
+ showToast(agent, { next: next, card: card })
817
+ announce(
818
+ "Factory completa: " +
819
+ agent.displayName +
820
+ " fabricado y agregado al roster. Click en la notificación para abrir su ficha.",
821
+ )
822
+ }
823
+
824
+ // ---------------------------------------------------------------- toasts
825
+ var toastTimers = []
826
+
827
+ function showToast(agent, options) {
828
+ var opts = options || {}
829
+ var next = opts.next
830
+ var toast = doc.createElement("div")
831
+ toast.className = "toast"
832
+ toast.setAttribute("role", "group")
833
+ toast.setAttribute("aria-label", "Factory completa: " + agent.displayName)
834
+
835
+ var detail = next
836
+ ? "match en " + next.cardId + " · " + next.before.pct + "% → " + next.after.pct + "%"
837
+ : "registrado en el roster"
838
+
839
+ toast.innerHTML =
840
+ '<button type="button" class="toast-open">' +
841
+ '<span class="toast-kicker">factory · completa</span>' +
842
+ '<span class="toast-name">' +
843
+ esc(agent.displayName) +
844
+ "</span>" +
845
+ '<span class="toast-body">' +
846
+ esc(detail) +
847
+ "</span>" +
848
+ '<span class="toast-cta">Abrir ficha <span aria-hidden="true">→</span></span>' +
849
+ "</button>" +
850
+ '<button type="button" class="toast-close" aria-label="Cerrar notificación">×</button>' +
851
+ '<span class="toast-timer" aria-hidden="true"></span>'
852
+
853
+ var dismiss = function () {
854
+ if (toast.classList.contains("is-leaving")) return
855
+ toast.classList.add("is-leaving")
856
+ setTimeout(function () {
857
+ if (toast.parentNode) toast.parentNode.removeChild(toast)
858
+ }, 240)
859
+ }
860
+
861
+ $(".toast-open", toast).addEventListener("click", function () {
862
+ openAgent(agent)
863
+ dismiss()
864
+ })
865
+ $(".toast-close", toast).addEventListener("click", dismiss)
866
+
867
+ els.toasts.appendChild(toast)
868
+ while (els.toasts.children.length > 3) {
869
+ els.toasts.removeChild(els.toasts.firstChild)
870
+ }
871
+ var timer = setTimeout(dismiss, 12000)
872
+ toastTimers.push(timer)
873
+ toast.addEventListener("mouseenter", function () {
874
+ clearTimeout(timer)
875
+ })
876
+ toast.addEventListener("focusin", function () {
877
+ clearTimeout(timer)
878
+ })
879
+ announce("Notificación: " + agent.displayName + " listo. Enter para abrir su ficha.")
880
+ }
881
+
882
+ // ---------------------------------------------------------------- modal
883
+ function toRepoRelative(path) {
884
+ if (!path) return ""
885
+ var normalized = String(path).replace(/\\/g, "/")
886
+ var marker = normalized.indexOf(".opencode/")
887
+ if (marker >= 0) return normalized.slice(marker)
888
+ var agentMarker = normalized.indexOf("agents/")
889
+ if (agentMarker >= 0) return ".opencode/" + normalized.slice(agentMarker)
890
+ return normalized.replace(/^\.?\//, "")
891
+ }
892
+
893
+ async function resolveFicha(agent) {
894
+ var rel = toRepoRelative(agent.fichaPath)
895
+ var snap = app.snapshot
896
+ if (snap && snap.fichas && rel && snap.fichas[rel]) {
897
+ return { content: snap.fichas[rel].content, origin: "snapshot" }
898
+ }
899
+ if (app.sourceKind === "live" && rel) {
900
+ try {
901
+ var response = await fetch("/api/ficha?path=" + encodeURIComponent(rel), {
902
+ cache: "no-store",
903
+ })
904
+ if (response.ok) {
905
+ var data = await response.json()
906
+ if (data && data.content) return { content: data.content, origin: "disco" }
907
+ }
908
+ } catch (error) {
909
+ /* fall through */
910
+ }
911
+ }
912
+ if (rel) {
913
+ try {
914
+ var raw = await fetch("../" + rel, { cache: "no-store" })
915
+ if (raw.ok) {
916
+ var text = await raw.text()
917
+ if (text && text.trim().length > 0) return { content: text, origin: "disco" }
918
+ }
919
+ } catch (error) {
920
+ /* fall through */
921
+ }
922
+ }
923
+ var card = cardById(agent.source && agent.source.fabricatedFor)
924
+ return {
925
+ content: Core.renderFichaMarkdown(agent, card),
926
+ origin: "proyectada",
927
+ }
928
+ }
929
+
930
+ function inlineCode(text) {
931
+ return esc(text).replace(/`([^`]+)`/g, function (_, code) {
932
+ return "<code>" + code + "</code>"
933
+ })
934
+ }
935
+
936
+ function renderMarkdown(container, markdown) {
937
+ container.innerHTML = ""
938
+ var lines = String(markdown || "").split("\n")
939
+ var i = 0
940
+ if (lines[0] === "---") {
941
+ var front = []
942
+ i = 1
943
+ while (i < lines.length && lines[i] !== "---") {
944
+ front.push(lines[i])
945
+ i += 1
946
+ }
947
+ i += 1
948
+ var pre = doc.createElement("pre")
949
+ pre.className = "md-front"
950
+ pre.textContent = front.join("\n")
951
+ container.appendChild(pre)
952
+ }
953
+ var list = null
954
+ for (; i < lines.length; i += 1) {
955
+ var line = lines[i]
956
+ if (/^\s*$/.test(line)) {
957
+ list = null
958
+ continue
959
+ }
960
+ if (/^#\s/.test(line)) {
961
+ var h3 = doc.createElement("h3")
962
+ h3.textContent = line.replace(/^#\s+/, "")
963
+ container.appendChild(h3)
964
+ list = null
965
+ continue
966
+ }
967
+ if (/^##\s/.test(line)) {
968
+ var h4 = doc.createElement("h4")
969
+ h4.textContent = line.replace(/^##\s+/, "")
970
+ container.appendChild(h4)
971
+ list = null
972
+ continue
973
+ }
974
+ if (/^\s*[-*]\s/.test(line)) {
975
+ if (!list) {
976
+ list = doc.createElement("ul")
977
+ container.appendChild(list)
978
+ }
979
+ var li = doc.createElement("li")
980
+ li.innerHTML = inlineCode(line.replace(/^\s*[-*]\s+/, ""))
981
+ list.appendChild(li)
982
+ continue
983
+ }
984
+ var p = doc.createElement("p")
985
+ p.innerHTML = inlineCode(line)
986
+ container.appendChild(p)
987
+ list = null
988
+ }
989
+ }
990
+
991
+ async function openAgent(agent) {
992
+ var card = cardById(agent.source && agent.source.fabricatedFor)
993
+ els.dialogAvatar.textContent = initials(agent.displayName)
994
+ els.dialogTitle.textContent = agent.displayName
995
+ els.dialogSub.textContent = agent.id + " · " + agent.role
996
+ els.dialogMeta.innerHTML =
997
+ agent.capabilities
998
+ .slice(0, 8)
999
+ .map(function (cap) {
1000
+ return '<span class="cap">' + esc(cap) + "</span>"
1001
+ })
1002
+ .join("") +
1003
+ (card
1004
+ ? '<span class="cap" data-kind="missing">card ' + esc(card.id) + "</span>"
1005
+ : "") +
1006
+ (agent.source && agent.source.kind === "factory"
1007
+ ? '<span class="badge" data-state="in_progress">fabricado</span>'
1008
+ : "")
1009
+ els.dialogBody.innerHTML = '<p class="md-front">cargando ficha…</p>'
1010
+ els.dialogOrigin.textContent = ""
1011
+ if (typeof els.dialog.showModal === "function") els.dialog.showModal()
1012
+ else els.dialog.setAttribute("open", "open")
1013
+
1014
+ var resolved = await resolveFicha(agent)
1015
+ app.fichaOrigin = resolved.origin
1016
+ renderMarkdown(els.dialogBody, resolved.content)
1017
+ els.dialogOrigin.textContent =
1018
+ resolved.origin === "proyectada"
1019
+ ? "ficha proyectada (no escrita en disco)"
1020
+ : "ficha desde " + resolved.origin + " · " + toRepoRelative(agent.fichaPath)
1021
+ els.btnCopy.setAttribute(
1022
+ "data-copy",
1023
+ resolved.content,
1024
+ )
1025
+ announce("Ficha de " + agent.displayName + " abierta.")
1026
+ }
1027
+
1028
+ // ---------------------------------------------------------------- controls
1029
+ function togglePause() {
1030
+ app.paused = !app.paused
1031
+ els.btnPause.setAttribute("aria-pressed", String(app.paused))
1032
+ els.btnPause.querySelector(".btn-label").textContent = app.paused
1033
+ ? "Reanudar"
1034
+ : "Pausar"
1035
+ els.pollChip.textContent = app.paused ? "poll pausado" : "poll " + app.interval + "ms"
1036
+ announce(app.paused ? "Polling pausado." : "Polling reanudado.")
1037
+ }
1038
+
1039
+ function applyTheme(theme) {
1040
+ root.setAttribute("data-theme", theme)
1041
+ try {
1042
+ localStorage.setItem(STORAGE_THEME, theme)
1043
+ } catch (error) {
1044
+ /* private mode */
1045
+ }
1046
+ var dark = theme === "dark"
1047
+ els.btnTheme.setAttribute("aria-pressed", String(dark))
1048
+ els.btnTheme.setAttribute(
1049
+ "aria-label",
1050
+ dark ? "Cambiar a tema claro" : "Cambiar a tema oscuro",
1051
+ )
1052
+ els.btnTheme.querySelector(".btn-label").textContent = dark ? "Claro" : "Oscuro"
1053
+ }
1054
+
1055
+ function initTheme() {
1056
+ var stored = null
1057
+ try {
1058
+ stored = localStorage.getItem(STORAGE_THEME)
1059
+ } catch (error) {
1060
+ stored = null
1061
+ }
1062
+ var prefersLight = window.matchMedia("(prefers-color-scheme: light)").matches
1063
+ applyTheme(stored || (prefersLight ? "light" : "dark"))
1064
+ }
1065
+
1066
+ function copyFicha() {
1067
+ var content = els.btnCopy.getAttribute("data-copy") || ""
1068
+ var done = function () {
1069
+ els.btnCopy.querySelector(".btn-label").textContent = "Copiada"
1070
+ setTimeout(function () {
1071
+ els.btnCopy.querySelector(".btn-label").textContent = "Copiar ficha"
1072
+ }, 1800)
1073
+ }
1074
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1075
+ navigator.clipboard.writeText(content).then(done, function () {
1076
+ announce("No se pudo copiar la ficha.")
1077
+ })
1078
+ return
1079
+ }
1080
+ announce("El navegador no permite copiar; seleccioná el texto a mano.")
1081
+ }
1082
+
1083
+ // ---------------------------------------------------------------- polling
1084
+ function startPolling() {
1085
+ setInterval(function () {
1086
+ if (app.paused || app.running) return
1087
+ app.tick += 1
1088
+ var rosterTurn = app.tick % 3 === 0
1089
+ sync({ roster: rosterTurn }).catch(function () {})
1090
+ }, 800)
1091
+ }
1092
+
1093
+ function wire() {
1094
+ els.btnFabricate.addEventListener("click", function () {
1095
+ var gap = focusGap()
1096
+ fabricate(gap ? gap.cardId : null)
1097
+ })
1098
+ els.btnPause.addEventListener("click", togglePause)
1099
+ els.btnResync.addEventListener("click", function () {
1100
+ sync({ manual: true })
1101
+ })
1102
+ els.btnTheme.addEventListener("click", function () {
1103
+ var next = root.getAttribute("data-theme") === "dark" ? "light" : "dark"
1104
+ applyTheme(next)
1105
+ announce("Tema " + next + " aplicado.")
1106
+ })
1107
+ els.btnCopy.addEventListener("click", copyFicha)
1108
+ $("#btn-dialog-close").addEventListener("click", function () {
1109
+ els.dialog.close()
1110
+ })
1111
+ els.dialog.addEventListener("click", function (event) {
1112
+ if (event.target === els.dialog) els.dialog.close()
1113
+ })
1114
+
1115
+ doc.addEventListener("keydown", function (event) {
1116
+ var tag = (event.target && event.target.tagName) || ""
1117
+ var typing = /INPUT|TEXTAREA|SELECT/.test(tag)
1118
+ if (typing) return
1119
+ if (event.key === "Escape") {
1120
+ if (els.dialog.open) return
1121
+ var keys = $(".keys[open]")
1122
+ if (keys) keys.removeAttribute("open")
1123
+ return
1124
+ }
1125
+ if (els.dialog.open) return
1126
+ if (event.key === " " && !/BUTTON|SUMMARY|A/.test(tag)) {
1127
+ event.preventDefault()
1128
+ togglePause()
1129
+ return
1130
+ }
1131
+ if (event.key === "r" || event.key === "R") {
1132
+ event.preventDefault()
1133
+ sync({ manual: true })
1134
+ return
1135
+ }
1136
+ if (event.key === "f" || event.key === "F") {
1137
+ event.preventDefault()
1138
+ if (!els.btnFabricate.disabled) els.btnFabricate.click()
1139
+ return
1140
+ }
1141
+ if (event.key === "?") {
1142
+ event.preventDefault()
1143
+ var details = $(".keys")
1144
+ var open = details.hasAttribute("open")
1145
+ if (open) details.removeAttribute("open")
1146
+ else details.setAttribute("open", "open")
1147
+ }
1148
+ })
1149
+ }
1150
+
1151
+ // ---------------------------------------------------------------- boot
1152
+ async function boot() {
1153
+ initTheme()
1154
+ wire()
1155
+ resetSequence()
1156
+ startPolling()
1157
+
1158
+ await sync({ manual: false })
1159
+ if (!app.snapshot) {
1160
+ els.btnFabricate.disabled = true
1161
+ return
1162
+ }
1163
+
1164
+ var params = new URLSearchParams(window.location.search)
1165
+ var requested = params.get("card")
1166
+ if (requested) {
1167
+ app.focusId = requested
1168
+ recompute()
1169
+ renderAll()
1170
+ }
1171
+
1172
+ var autoRun = params.get("auto") === "1" || params.get("fabricate") === "1"
1173
+ if (autoRun && app.preview) {
1174
+ setTimeout(function () {
1175
+ fabricate(app.focusId)
1176
+ }, 700)
1177
+ }
1178
+ }
1179
+
1180
+ if (document.readyState === "loading") {
1181
+ document.addEventListener("DOMContentLoaded", boot)
1182
+ } else {
1183
+ boot()
1184
+ }
1185
+ })()