oficina-opencode 3.0.5 → 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 (145) hide show
  1. package/README.md +240 -208
  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 +148 -26
  43. package/dist/cli.js.map +1 -1
  44. package/dist/core/bus.js +3 -3
  45. package/dist/core/bus.js.map +1 -1
  46. package/dist/core/catalog.d.ts +9 -1
  47. package/dist/core/catalog.d.ts.map +1 -1
  48. package/dist/core/catalog.js +77 -2
  49. package/dist/core/catalog.js.map +1 -1
  50. package/dist/core/dashboard.d.ts +33 -0
  51. package/dist/core/dashboard.d.ts.map +1 -0
  52. package/dist/core/dashboard.js +115 -0
  53. package/dist/core/dashboard.js.map +1 -0
  54. package/dist/core/dispatch.d.ts +1 -1
  55. package/dist/core/dispatch.d.ts.map +1 -1
  56. package/dist/core/dispatch.js +84 -12
  57. package/dist/core/dispatch.js.map +1 -1
  58. package/dist/core/docsync.d.ts +0 -9
  59. package/dist/core/docsync.d.ts.map +1 -1
  60. package/dist/core/docsync.js +12 -15
  61. package/dist/core/docsync.js.map +1 -1
  62. package/dist/core/environment.d.ts +28 -0
  63. package/dist/core/environment.d.ts.map +1 -0
  64. package/dist/core/environment.js +80 -0
  65. package/dist/core/environment.js.map +1 -0
  66. package/dist/core/factory.d.ts +16 -0
  67. package/dist/core/factory.d.ts.map +1 -1
  68. package/dist/core/factory.js +143 -0
  69. package/dist/core/factory.js.map +1 -1
  70. package/dist/core/fsx.d.ts +20 -2
  71. package/dist/core/fsx.d.ts.map +1 -1
  72. package/dist/core/fsx.js +10 -6
  73. package/dist/core/fsx.js.map +1 -1
  74. package/dist/core/gates.d.ts +23 -1
  75. package/dist/core/gates.d.ts.map +1 -1
  76. package/dist/core/gates.js +83 -8
  77. package/dist/core/gates.js.map +1 -1
  78. package/dist/core/leases.d.ts.map +1 -1
  79. package/dist/core/leases.js +4 -1
  80. package/dist/core/leases.js.map +1 -1
  81. package/dist/core/mesa.d.ts +1 -6
  82. package/dist/core/mesa.d.ts.map +1 -1
  83. package/dist/core/mesa.js +1 -6
  84. package/dist/core/mesa.js.map +1 -1
  85. package/dist/core/office.d.ts +3 -29
  86. package/dist/core/office.d.ts.map +1 -1
  87. package/dist/core/office.js +77 -90
  88. package/dist/core/office.js.map +1 -1
  89. package/dist/core/paths.d.ts.map +1 -1
  90. package/dist/core/paths.js +6 -1
  91. package/dist/core/paths.js.map +1 -1
  92. package/dist/core/retry.d.ts +2 -8
  93. package/dist/core/retry.d.ts.map +1 -1
  94. package/dist/core/retry.js +3 -34
  95. package/dist/core/retry.js.map +1 -1
  96. package/dist/core/roster.js +3 -3
  97. package/dist/core/roster.js.map +1 -1
  98. package/dist/core/sandbox.d.ts +38 -1
  99. package/dist/core/sandbox.d.ts.map +1 -1
  100. package/dist/core/sandbox.js +119 -15
  101. package/dist/core/sandbox.js.map +1 -1
  102. package/dist/core/state.d.ts +2 -2
  103. package/dist/core/state.d.ts.map +1 -1
  104. package/dist/core/state.js +5 -33
  105. package/dist/core/state.js.map +1 -1
  106. package/dist/core/stores.d.ts +3 -13
  107. package/dist/core/stores.d.ts.map +1 -1
  108. package/dist/core/stores.js +8 -32
  109. package/dist/core/stores.js.map +1 -1
  110. package/dist/core/systemPrompt.d.ts.map +1 -1
  111. package/dist/core/systemPrompt.js +4 -1
  112. package/dist/core/systemPrompt.js.map +1 -1
  113. package/dist/core/tripwire.d.ts +22 -8
  114. package/dist/core/tripwire.d.ts.map +1 -1
  115. package/dist/core/tripwire.js +71 -26
  116. package/dist/core/tripwire.js.map +1 -1
  117. package/dist/core/types.d.ts +6 -14
  118. package/dist/core/types.d.ts.map +1 -1
  119. package/dist/core/types.js +11 -3
  120. package/dist/core/types.js.map +1 -1
  121. package/dist/hooks/scopeGuard.d.ts +15 -1
  122. package/dist/hooks/scopeGuard.d.ts.map +1 -1
  123. package/dist/hooks/scopeGuard.js +88 -73
  124. package/dist/hooks/scopeGuard.js.map +1 -1
  125. package/dist/tools/index.d.ts +9 -1
  126. package/dist/tools/index.d.ts.map +1 -1
  127. package/dist/tools/index.js +29 -81
  128. package/dist/tools/index.js.bak +972 -0
  129. package/dist/tools/index.js.map +1 -1
  130. package/dist/v1.d.ts.map +1 -1
  131. package/dist/v1.js +30 -36
  132. package/dist/v1.js.map +1 -1
  133. package/dist/v2.d.ts +5 -0
  134. package/dist/v2.d.ts.map +1 -1
  135. package/dist/v2.js +79 -46
  136. package/dist/v2.js.bak +517 -0
  137. package/dist/v2.js.map +1 -1
  138. package/docs/GARANTIAS.md +2 -2
  139. package/docs/PRUEBA-E2E.md +4 -4
  140. package/docs/QUE-ES.md +4 -5
  141. package/docs/SEGURIDAD-REPO.md +83 -0
  142. package/docs/assets/banner.svg +42 -0
  143. package/docs/catalog-facts.json +3 -3
  144. package/docs/superpowers/specs/2026-10-29-factory-skill-creator-and-remove-budget.md +182 -0
  145. package/package.json +14 -6
@@ -0,0 +1,1008 @@
1
+ <!DOCTYPE html>
2
+ <html lang="es" data-theme="dark">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Panel del Jefe · Commander / Integrator / Architect</title>
7
+ <meta name="description" content="Panel de intervencion del jefe para la Oficina de Agents: decisiones de Commander, Integrator y Architect con razon, siguiente accion y links a card y artifacts." />
8
+ <meta name="color-scheme" content="dark light" />
9
+ <style>
10
+ /* ───────────────────────────────────────────────────────────────────────────
11
+ Tokens. OKLCH, contraste AA sobre el fondo de cada tema.
12
+ ─────────────────────────────────────────────────────────────────────────── */
13
+ :root {
14
+ --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
15
+ --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
16
+
17
+ --radius-sm: 6px;
18
+ --radius-md: 10px;
19
+ --radius-lg: 14px;
20
+
21
+ --space-1: 4px;
22
+ --space-2: 8px;
23
+ --space-3: 12px;
24
+ --space-4: 16px;
25
+ --space-5: 24px;
26
+ --space-6: 32px;
27
+
28
+ --cmd: oklch(0.78 0.14 78);
29
+ --int: oklch(0.78 0.11 195);
30
+ --arc: oklch(0.76 0.14 305);
31
+ --ok: oklch(0.76 0.15 150);
32
+ --warn: oklch(0.8 0.15 80);
33
+ --bad: oklch(0.68 0.19 25);
34
+ }
35
+
36
+ html[data-theme="dark"] {
37
+ --bg: oklch(0.17 0.012 265);
38
+ --bg-raised: oklch(0.215 0.014 265);
39
+ --bg-sunken: oklch(0.145 0.012 265);
40
+ --line: oklch(0.32 0.016 265);
41
+ --line-strong: oklch(0.44 0.02 265);
42
+ --fg: oklch(0.96 0.004 265);
43
+ --fg-muted: oklch(0.79 0.012 265);
44
+ --fg-faint: oklch(0.68 0.014 265);
45
+ --accent: oklch(0.82 0.13 195);
46
+ --focus: oklch(0.88 0.16 195);
47
+ --shadow: 0 12px 32px oklch(0 0 0 / 0.42);
48
+ --chip-bg: oklch(0.28 0.016 265);
49
+ }
50
+
51
+ html[data-theme="light"] {
52
+ --bg: oklch(0.985 0.003 265);
53
+ --bg-raised: oklch(1 0 0);
54
+ --bg-sunken: oklch(0.955 0.006 265);
55
+ --line: oklch(0.87 0.01 265);
56
+ --line-strong: oklch(0.74 0.014 265);
57
+ --fg: oklch(0.24 0.02 265);
58
+ --fg-muted: oklch(0.43 0.02 265);
59
+ --fg-faint: oklch(0.52 0.018 265);
60
+ --accent: oklch(0.52 0.13 220);
61
+ --focus: oklch(0.52 0.16 240);
62
+ --shadow: 0 10px 28px oklch(0.4 0.02 265 / 0.14);
63
+ --chip-bg: oklch(0.94 0.008 265);
64
+
65
+ --cmd: oklch(0.55 0.13 62);
66
+ --int: oklch(0.5 0.11 215);
67
+ --arc: oklch(0.5 0.16 300);
68
+ --ok: oklch(0.5 0.14 150);
69
+ --warn: oklch(0.55 0.14 70);
70
+ --bad: oklch(0.52 0.19 25);
71
+ }
72
+
73
+ *, *::before, *::after { box-sizing: border-box; }
74
+
75
+ html, body { height: 100%; }
76
+
77
+ body {
78
+ margin: 0;
79
+ background: var(--bg);
80
+ color: var(--fg);
81
+ font: 15px/1.5 var(--font-sans);
82
+ -webkit-font-smoothing: antialiased;
83
+ display: flex;
84
+ flex-direction: column;
85
+ min-height: 100%;
86
+ }
87
+
88
+ a { color: var(--accent); text-underline-offset: 2px; }
89
+ a:hover { text-decoration-thickness: 2px; }
90
+
91
+ :focus-visible {
92
+ outline: 2px solid var(--focus);
93
+ outline-offset: 2px;
94
+ border-radius: var(--radius-sm);
95
+ }
96
+
97
+ .skip-link {
98
+ position: absolute; left: var(--space-4); top: -100px;
99
+ background: var(--bg-raised); color: var(--fg);
100
+ padding: var(--space-2) var(--space-4); border-radius: var(--radius-md);
101
+ border: 1px solid var(--line-strong); z-index: 60;
102
+ }
103
+ .skip-link:focus { top: var(--space-3); }
104
+
105
+ .visually-hidden {
106
+ position: absolute !important; width: 1px; height: 1px;
107
+ padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
108
+ }
109
+
110
+ /* ── Header ──────────────────────────────────────────────────────────────── */
111
+ header.top {
112
+ position: sticky; top: 0; z-index: 30;
113
+ background: color-mix(in oklch, var(--bg) 88%, transparent);
114
+ backdrop-filter: blur(10px);
115
+ border-bottom: 1px solid var(--line);
116
+ padding: var(--space-3) var(--space-4);
117
+ }
118
+
119
+ .top-row {
120
+ display: flex; align-items: center; gap: var(--space-3);
121
+ flex-wrap: wrap;
122
+ }
123
+
124
+ .brand { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
125
+ .brand h1 {
126
+ margin: 0; font-size: 1.06rem; font-weight: 650; letter-spacing: -0.01em;
127
+ display: flex; align-items: center; gap: var(--space-2);
128
+ }
129
+ .brand .sub {
130
+ color: var(--fg-faint); font-size: 0.78rem; font-family: var(--font-mono);
131
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 62vw;
132
+ }
133
+
134
+ .mark {
135
+ width: 10px; height: 10px; border-radius: 3px; flex: none;
136
+ background: linear-gradient(135deg, var(--cmd), var(--int) 55%, var(--arc));
137
+ }
138
+
139
+ .top-actions { margin-left: auto; display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
140
+
141
+ .badge-mode {
142
+ font-family: var(--font-mono); font-size: 0.72rem;
143
+ padding: 3px 8px; border-radius: 999px;
144
+ border: 1px solid var(--line-strong); color: var(--fg-muted);
145
+ background: var(--chip-bg);
146
+ display: inline-flex; align-items: center; gap: 6px;
147
+ }
148
+ .badge-mode .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--warn); flex: none; }
149
+ .badge-mode[data-mode="live"] .dot { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in oklch, var(--ok) 25%, transparent); }
150
+ .badge-mode[data-mode="files"] .dot { background: var(--int); }
151
+ .badge-mode[data-mode="demo"] .dot { background: var(--bad); }
152
+ .badge-mode[data-paused="true"] .dot { background: var(--fg-faint); box-shadow: none; }
153
+
154
+ button, select, input[type="search"], input[type="text"] {
155
+ font: inherit; color: var(--fg);
156
+ background: var(--bg-raised);
157
+ border: 1px solid var(--line-strong);
158
+ border-radius: var(--radius-sm);
159
+ padding: 6px 10px;
160
+ }
161
+ button { cursor: pointer; }
162
+ button:hover { border-color: var(--accent); }
163
+ button[aria-pressed="true"] { background: var(--chip-bg); border-color: var(--accent); }
164
+ button.icon { min-width: 34px; text-align: center; }
165
+
166
+ /* ── Filtros ─────────────────────────────────────────────────────────────── */
167
+ .filters {
168
+ display: flex; gap: var(--space-2); align-items: flex-end; flex-wrap: wrap;
169
+ padding: var(--space-3) var(--space-4);
170
+ border-bottom: 1px solid var(--line);
171
+ background: var(--bg-sunken);
172
+ }
173
+ .field { display: flex; flex-direction: column; gap: 3px; }
174
+ .field > label {
175
+ font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase;
176
+ color: var(--fg-faint); font-weight: 600;
177
+ }
178
+ .field.search { flex: 1 1 220px; min-width: 160px; }
179
+ .field.search input { width: 100%; }
180
+ .filters .counter { margin-left: auto; color: var(--fg-muted); font-size: 0.82rem; font-variant-numeric: tabular-nums; }
181
+ .toggle {
182
+ display: inline-flex; align-items: center; gap: 6px;
183
+ font-size: 0.84rem; color: var(--fg-muted); cursor: pointer;
184
+ padding-bottom: 6px;
185
+ }
186
+ .toggle input { accent-color: var(--accent); width: 16px; height: 16px; }
187
+
188
+ /* ── Grid de secciones ───────────────────────────────────────────────────── */
189
+ main { flex: 1 1 auto; padding: var(--space-4); }
190
+
191
+ .board {
192
+ display: grid;
193
+ grid-template-columns: repeat(3, minmax(0, 1fr));
194
+ gap: var(--space-3);
195
+ align-items: start;
196
+ }
197
+
198
+ .section {
199
+ background: var(--bg-sunken);
200
+ border: 1px solid var(--line);
201
+ border-radius: var(--radius-lg);
202
+ display: flex; flex-direction: column;
203
+ min-height: 200px;
204
+ overflow: hidden;
205
+ }
206
+
207
+ .section > header {
208
+ display: flex; align-items: baseline; gap: var(--space-2);
209
+ padding: var(--space-3) var(--space-4);
210
+ border-bottom: 1px solid var(--line);
211
+ border-top: 3px solid var(--tone);
212
+ background: linear-gradient(180deg, color-mix(in oklch, var(--tone) 12%, transparent), transparent);
213
+ }
214
+ .section[data-coordinator="commander"] { --tone: var(--cmd); }
215
+ .section[data-coordinator="integrator"] { --tone: var(--int); }
216
+ .section[data-coordinator="architect"] { --tone: var(--arc); }
217
+
218
+ .section h2 {
219
+ margin: 0; font-size: 0.98rem; font-weight: 650; letter-spacing: -0.01em;
220
+ display: flex; align-items: center; gap: var(--space-2);
221
+ }
222
+ .section h2 .sigil {
223
+ font-family: var(--font-mono); font-size: 0.64rem; letter-spacing: 0.08em;
224
+ padding: 2px 6px; border-radius: 4px;
225
+ color: var(--bg); background: var(--tone); font-weight: 700;
226
+ }
227
+ .section .scope {
228
+ display: block; color: var(--fg-faint); font-size: 0.75rem;
229
+ font-family: var(--font-mono); margin-top: 2px;
230
+ }
231
+ .section .count {
232
+ margin-left: auto; font-family: var(--font-mono); font-size: 0.78rem;
233
+ color: var(--fg-muted); font-variant-numeric: tabular-nums;
234
+ padding: 1px 8px; border-radius: 999px; background: var(--chip-bg);
235
+ border: 1px solid var(--line);
236
+ }
237
+
238
+ .decisions { list-style: none; margin: 0; padding: var(--space-2); display: flex; flex-direction: column; gap: var(--space-2); }
239
+ .decisions:empty { padding: var(--space-3); }
240
+
241
+ /* ── Tarjeta de decision ─────────────────────────────────────────────────── */
242
+ .decision {
243
+ position: relative;
244
+ background: var(--bg-raised);
245
+ border: 1px solid var(--line);
246
+ border-left: 3px solid var(--tone, var(--line-strong));
247
+ border-radius: var(--radius-md);
248
+ padding: var(--space-3);
249
+ display: grid; gap: 6px;
250
+ transition: border-color 120ms ease, transform 120ms ease;
251
+ }
252
+ .decision:hover { border-color: var(--line-strong); }
253
+ .decision:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
254
+ .decision[data-priority="critical"] { box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--bad) 45%, transparent); }
255
+ .decision[data-priority="high"] { box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--warn) 32%, transparent); }
256
+
257
+ .dec-top { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
258
+
259
+ .stamp {
260
+ font-family: var(--font-mono); font-size: 0.72rem; color: var(--fg-faint);
261
+ display: inline-flex; align-items: baseline; gap: 5px;
262
+ font-variant-numeric: tabular-nums;
263
+ }
264
+ .stamp .rel { color: var(--fg-muted); }
265
+
266
+ .chip {
267
+ font-family: var(--font-mono); font-size: 0.7rem; line-height: 1.6;
268
+ padding: 1px 6px; border-radius: 999px;
269
+ background: var(--chip-bg); border: 1px solid var(--line);
270
+ color: var(--fg-muted); white-space: nowrap;
271
+ }
272
+ .chip.actor { color: var(--fg); border-color: color-mix(in oklch, var(--tone) 45%, var(--line)); }
273
+ .chip.actor .role { color: var(--fg-faint); }
274
+ .chip.type { letter-spacing: 0.02em; }
275
+ .chip.prio-critical { color: var(--bad); border-color: color-mix(in oklch, var(--bad) 55%, var(--line)); font-weight: 700; }
276
+ .chip.prio-high { color: var(--warn); border-color: color-mix(in oklch, var(--warn) 45%, var(--line)); }
277
+
278
+ .card-link {
279
+ font-family: var(--font-mono); font-size: 0.72rem; font-weight: 700;
280
+ background: transparent; border: 1px dashed var(--line-strong);
281
+ color: var(--accent); padding: 0 6px; border-radius: var(--radius-sm);
282
+ text-decoration: underline; text-underline-offset: 2px;
283
+ }
284
+ .card-link:hover { border-style: solid; }
285
+ .no-card { font-family: var(--font-mono); font-size: 0.72rem; color: var(--fg-faint); }
286
+
287
+ .dec-title { margin: 0; font-size: 0.9rem; font-weight: 600; line-height: 1.35; }
288
+
289
+ .dec-field { display: grid; grid-template-columns: 62px 1fr; gap: var(--space-2); align-items: start; }
290
+ .dec-field > dt {
291
+ font-size: 0.66rem; letter-spacing: 0.06em; text-transform: uppercase;
292
+ color: var(--fg-faint); font-weight: 700; padding-top: 2px;
293
+ }
294
+ .dec-field > dd { margin: 0; font-size: 0.84rem; color: var(--fg-muted); }
295
+ .dec-field > dd.mono { font-family: var(--font-mono); font-size: 0.78rem; }
296
+
297
+ .next {
298
+ display: grid; grid-template-columns: 62px 1fr; gap: var(--space-2);
299
+ padding-top: 6px; border-top: 1px dashed var(--line);
300
+ }
301
+ .next > dt {
302
+ font-size: 0.66rem; letter-spacing: 0.06em; text-transform: uppercase;
303
+ color: var(--fg-faint); font-weight: 700; padding-top: 2px;
304
+ }
305
+ .next > dd { margin: 0; font-size: 0.84rem; color: var(--fg); }
306
+ .next > dd::before { content: "→ "; color: var(--tone); font-weight: 700; }
307
+
308
+ .dec-links { display: flex; gap: var(--space-2); flex-wrap: wrap; font-size: 0.76rem; }
309
+ .dec-links a, .dec-links button.linkish {
310
+ color: var(--fg-muted); background: none; border: 0; padding: 0;
311
+ text-decoration: underline; text-underline-offset: 2px; cursor: pointer; font-size: inherit;
312
+ }
313
+ .dec-links a:hover, .dec-links button.linkish:hover { color: var(--accent); }
314
+
315
+ .empty {
316
+ margin: 0; padding: var(--space-5) var(--space-4); text-align: center;
317
+ color: var(--fg-faint); font-size: 0.85rem;
318
+ }
319
+
320
+ /* ── Dialog ──────────────────────────────────────────────────────────────── */
321
+ dialog {
322
+ border: 1px solid var(--line-strong);
323
+ border-radius: var(--radius-lg);
324
+ background: var(--bg-raised);
325
+ color: var(--fg);
326
+ padding: 0;
327
+ max-width: min(680px, 94vw);
328
+ width: 100%;
329
+ box-shadow: var(--shadow);
330
+ }
331
+ dialog::backdrop { background: oklch(0 0 0 / 0.55); backdrop-filter: blur(2px); }
332
+ .dlg-head {
333
+ display: flex; align-items: center; gap: var(--space-2);
334
+ padding: var(--space-3) var(--space-4);
335
+ border-bottom: 1px solid var(--line);
336
+ position: sticky; top: 0; background: var(--bg-raised);
337
+ }
338
+ .dlg-head h2 { margin: 0; font-size: 1rem; }
339
+ .dlg-head .close { margin-left: auto; }
340
+ .dlg-body { padding: var(--space-4); display: grid; gap: var(--space-3); max-height: 74vh; overflow: auto; }
341
+ .kv { display: grid; grid-template-columns: 96px 1fr; gap: var(--space-2); font-size: 0.86rem; }
342
+ .kv > dt { color: var(--fg-faint); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 700; padding-top: 2px; }
343
+ .kv > dd { margin: 0; }
344
+ .state-badge {
345
+ font-family: var(--font-mono); font-size: 0.72rem; padding: 1px 7px; border-radius: 999px;
346
+ border: 1px solid var(--line-strong); color: var(--fg-muted);
347
+ }
348
+ .state-badge[data-state="done"] { color: var(--ok); border-color: color-mix(in oklch, var(--ok) 55%, var(--line)); }
349
+ .state-badge[data-state="rework"], .state-badge[data-state="blocked"] { color: var(--bad); border-color: color-mix(in oklch, var(--bad) 55%, var(--line)); }
350
+ .state-badge[data-state="escalated"] { color: var(--warn); border-color: color-mix(in oklch, var(--warn) 55%, var(--line)); }
351
+ .state-badge[data-state="in_progress"] { color: var(--int); border-color: color-mix(in oklch, var(--int) 55%, var(--line)); }
352
+ ul.clean { margin: 0; padding-left: 1.1em; }
353
+ ul.clean li { margin: 2px 0; }
354
+ .linkrow { display: flex; gap: var(--space-3); flex-wrap: wrap; font-size: 0.84rem; }
355
+ .hint { color: var(--fg-faint); font-size: 0.78rem; margin: 0; }
356
+
357
+ kbd {
358
+ font-family: var(--font-mono); font-size: 0.72rem; padding: 1px 5px;
359
+ border: 1px solid var(--line-strong); border-bottom-width: 2px;
360
+ border-radius: 4px; background: var(--bg-sunken); color: var(--fg-muted);
361
+ }
362
+
363
+ /* ── Footer ──────────────────────────────────────────────────────────────── */
364
+ footer.foot {
365
+ border-top: 1px solid var(--line);
366
+ padding: var(--space-2) var(--space-4) var(--space-4);
367
+ color: var(--fg-faint); font-size: 0.76rem;
368
+ display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: center;
369
+ font-family: var(--font-mono);
370
+ }
371
+ footer.foot .sep { color: var(--line-strong); }
372
+
373
+ /* ── Responsive: mobile = acordeon por seccion ───────────────────────────── */
374
+ @media (max-width: 1024px) {
375
+ .board { grid-template-columns: 1fr; }
376
+ .section { min-height: 0; }
377
+ .brand .sub { max-width: 100%; }
378
+ }
379
+
380
+ @media (max-width: 600px) {
381
+ main { padding: var(--space-2); }
382
+ .filters { padding: var(--space-2) var(--space-3); }
383
+ .field { flex: 1 1 46%; }
384
+ .field.search { flex-basis: 100%; }
385
+ .dec-field, .next { grid-template-columns: 1fr; gap: 2px; }
386
+ .dec-field > dt, .next > dt { padding-top: 4px; }
387
+ }
388
+
389
+ @media (prefers-reduced-motion: reduce) {
390
+ * { transition: none !important; animation: none !important; }
391
+ }
392
+ </style>
393
+ </head>
394
+ <body>
395
+ <a class="skip-link" href="#decisions">Saltar a las decisiones</a>
396
+
397
+ <header class="top">
398
+ <div class="top-row">
399
+ <div class="brand">
400
+ <h1><span class="mark" aria-hidden="true"></span> Panel del Jefe</h1>
401
+ <span class="sub" id="runline">Commander · Integrator · Architect — cargando…</span>
402
+ </div>
403
+ <div class="top-actions">
404
+ <span class="badge-mode" id="mode-badge" data-mode="snapshot" title="Origen de los datos">
405
+ <span class="dot" aria-hidden="true"></span><span id="mode-text">cargando</span>
406
+ </span>
407
+ <button type="button" id="btn-pause" aria-pressed="false" title="Pausar / reanudar polling (Espacio)">⏸ Pausar</button>
408
+ <button type="button" id="btn-refresh" title="Refrescar ahora (R)">↻</button>
409
+ <button type="button" id="btn-theme" class="icon" title="Tema claro / oscuro (T)" aria-label="Cambiar tema">◐</button>
410
+ <button type="button" id="btn-help" class="icon" title="Ayuda y atajos (?)" aria-label="Ayuda y atajos">?</button>
411
+ </div>
412
+ </div>
413
+ </header>
414
+
415
+ <div class="filters" role="search" aria-label="Filtros de decisiones">
416
+ <div class="field">
417
+ <label for="f-coordinator">Coordinador</label>
418
+ <select id="f-coordinator">
419
+ <option value="all">Todos</option>
420
+ <option value="commander">Commander</option>
421
+ <option value="integrator">Integrator</option>
422
+ <option value="architect">Architect</option>
423
+ </select>
424
+ </div>
425
+ <div class="field">
426
+ <label for="f-card">Card</label>
427
+ <select id="f-card"><option value="all">Todas</option></select>
428
+ </div>
429
+ <div class="field">
430
+ <label for="f-type">Tipo de decision</label>
431
+ <select id="f-type"><option value="all">Todos</option></select>
432
+ </div>
433
+ <div class="field search">
434
+ <label for="f-query">Buscar</label>
435
+ <input id="f-query" type="search" placeholder="razon, actor, card, siguiente accion…" autocomplete="off" />
436
+ </div>
437
+ <label class="toggle" for="f-only-decisions">
438
+ <input id="f-only-decisions" type="checkbox" checked />
439
+ Solo decisiones
440
+ </label>
441
+ <span class="counter" id="counter" aria-live="off">0 de 0</span>
442
+ </div>
443
+
444
+ <main>
445
+ <div class="board" id="board">
446
+ <section class="section" data-coordinator="commander" aria-labelledby="h-commander">
447
+ <header>
448
+ <div>
449
+ <h2 id="h-commander"><span class="sigil">CMD</span> Commander</h2>
450
+ <span class="scope">scope · prioridades · escalations</span>
451
+ </div>
452
+ <span class="count" id="count-commander" title="Decisiones en esta seccion">0</span>
453
+ </header>
454
+ <ul class="decisions" id="list-commander" aria-describedby="h-commander"></ul>
455
+ </section>
456
+
457
+ <section class="section" data-coordinator="integrator" aria-labelledby="h-integrator">
458
+ <header>
459
+ <div>
460
+ <h2 id="h-integrator"><span class="sigil">INT</span> Integrator</h2>
461
+ <span class="scope">routing · leases · gates · deadlocks</span>
462
+ </div>
463
+ <span class="count" id="count-integrator" title="Decisiones en esta seccion">0</span>
464
+ </header>
465
+ <ul class="decisions" id="list-integrator" aria-describedby="h-integrator"></ul>
466
+ </section>
467
+
468
+ <section class="section" data-coordinator="architect" aria-labelledby="h-architect">
469
+ <header>
470
+ <div>
471
+ <h2 id="h-architect"><span class="sigil">ARC</span> Architect</h2>
472
+ <span class="scope">tech decisions · patterns · contracts</span>
473
+ </div>
474
+ <span class="count" id="count-architect" title="Decisiones en esta seccion">0</span>
475
+ </header>
476
+ <ul class="decisions" id="list-architect" aria-describedby="h-architect"></ul>
477
+ </section>
478
+ </div>
479
+ </main>
480
+
481
+ <footer class="foot">
482
+ <span id="last-update">—</span>
483
+ <span class="sep">·</span>
484
+ <span id="source-note">—</span>
485
+ <span class="sep">·</span>
486
+ <span><kbd>?</kbd> atajos</span>
487
+ </footer>
488
+
489
+ <div id="announcer" class="visually-hidden" role="status" aria-live="polite" aria-atomic="true"></div>
490
+
491
+ <!-- Detalle de card -->
492
+ <dialog id="dlg-card" aria-labelledby="dlg-card-title">
493
+ <div class="dlg-head">
494
+ <h2 id="dlg-card-title">Card</h2>
495
+ <button type="button" class="close" id="dlg-card-close" aria-label="Cerrar detalle">✕</button>
496
+ </div>
497
+ <div class="dlg-body" id="dlg-card-body"></div>
498
+ </dialog>
499
+
500
+ <!-- Ayuda -->
501
+ <dialog id="dlg-help" aria-labelledby="dlg-help-title">
502
+ <div class="dlg-head">
503
+ <h2 id="dlg-help-title">Panel del Jefe — qué muestra y cómo se usa</h2>
504
+ <button type="button" class="close" id="dlg-help-close" aria-label="Cerrar ayuda">✕</button>
505
+ </div>
506
+ <div class="dlg-body">
507
+ <p class="hint">Tres secciones: <strong>Commander</strong> (scope, prioridades, escalations),
508
+ <strong>Integrator</strong> (routing, leases, gates, deadlocks) y
509
+ <strong>Architect</strong> (tech decisions, patterns, contracts). Cada decision muestra
510
+ timestamp, actor, card, decision, razon y siguiente accion, con enlace a la card y a sus artifacts.</p>
511
+ <dl class="kv">
512
+ <dt>Fuente</dt><dd>Decisiones de <code>state.json</code> (decisions, escalations, context.decisions) + mensajes de
513
+ <code>bus/coord-command</code> (Commander), <code>bus/coord-flow</code> (Integrator) y <code>bus/coord-arch</code> (Architect).</dd>
514
+ <dt>Atajos</dt><dd>
515
+ <kbd>j</kbd>/<kbd>k</kbd> decision siguiente/anterior · <kbd>Enter</kbd> abrir card ·
516
+ <kbd>1</kbd><kbd>2</kbd><kbd>3</kbd> ir a seccion · <kbd>/</kbd> buscar ·
517
+ <kbd>Espacio</kbd> pausar polling · <kbd>r</kbd> refrescar · <kbd>t</kbd> tema · <kbd>?</kbd> esta ayuda · <kbd>Esc</kbd> cerrar.
518
+ </dd>
519
+ <dt>Modos</dt><dd>
520
+ <code>live</code>: <code>node dashboard/scripts/serve.mjs</code> (lee <code>.oficina/</code> en vivo).
521
+ <code>archivos</code>: servidor estatico desde la raiz del repo (solo <code>state.json</code>).
522
+ <code>snapshot</code>: <code>node dashboard/scripts/snapshot.mjs</code> genera <code>dashboard/data/snapshot.json</code>.
523
+ <code>demo</code>: datos embebidos, sin <code>.oficina/</code>.
524
+ </dd>
525
+ </dl>
526
+ <p class="hint">El panel es de solo lectura: no escribe en <code>.oficina/</code> ni en <code>src/</code>.</p>
527
+ </div>
528
+ </dialog>
529
+
530
+ <noscript>
531
+ <p style="padding:1rem;color:#f88">Este panel necesita JavaScript habilitado para leer el estado de la oficina.</p>
532
+ </noscript>
533
+
534
+ <script src="./lib/leadership-model.js"></script>
535
+ <script>
536
+ "use strict";
537
+ (function () {
538
+ var L = window.OficinaLeadership;
539
+ var CONFIG = {
540
+ api: "/api/snapshot",
541
+ snapshotFile: "./data/snapshot.json",
542
+ stateFile: "../.oficina/state.json",
543
+ rosterFile: "../.oficina/roster.json",
544
+ pollMs: 1000,
545
+ };
546
+
547
+ var $ = function (id) { return document.getElementById(id); };
548
+
549
+ var state = {
550
+ snapshot: null,
551
+ items: [],
552
+ view: { commander: [], integrator: [], architect: [] },
553
+ filters: { coordinator: "all", card: "all", type: "all", query: "", onlyDecisions: true },
554
+ mode: "snapshot",
555
+ paused: false,
556
+ lastRenderSig: "",
557
+ lastTotal: 0,
558
+ firstLoad: true,
559
+ timer: null,
560
+ focusIndex: -1,
561
+ };
562
+
563
+ // ── Tema ───────────────────────────────────────────────────────────────────
564
+ var THEME_KEY = "oficina.leadership.theme";
565
+ function applyTheme(theme) {
566
+ document.documentElement.setAttribute("data-theme", theme);
567
+ try { localStorage.setItem(THEME_KEY, theme); } catch (e) { /* file:// sin storage */ }
568
+ }
569
+ function initTheme() {
570
+ var saved = null;
571
+ try { saved = localStorage.getItem(THEME_KEY); } catch (e) { /* noop */ }
572
+ if (!saved) saved = window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
573
+ applyTheme(saved);
574
+ }
575
+ function toggleTheme() {
576
+ applyTheme(document.documentElement.getAttribute("data-theme") === "dark" ? "light" : "dark");
577
+ }
578
+
579
+ // ── Utilidades DOM ─────────────────────────────────────────────────────────
580
+ function el(tag, props, children) {
581
+ var node = document.createElement(tag);
582
+ if (props) {
583
+ Object.keys(props).forEach(function (k) {
584
+ var v = props[k];
585
+ if (v === null || v === undefined || v === false) return;
586
+ if (k === "class") node.className = v;
587
+ else if (k === "text") node.textContent = v;
588
+ else if (k === "dataset") Object.keys(v).forEach(function (d) { node.dataset[d] = v[d]; });
589
+ else if (k === "attrs") Object.keys(v).forEach(function (a) { node.setAttribute(a, v[a]); });
590
+ else if (k.indexOf("on") === 0 && typeof v === "function") node.addEventListener(k.slice(2), v);
591
+ else node.setAttribute(k, v);
592
+ });
593
+ }
594
+ (children || []).forEach(function (c) {
595
+ if (c === null || c === undefined || c === false) return;
596
+ node.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
597
+ });
598
+ return node;
599
+ }
600
+
601
+ function announce(text) {
602
+ var a = $("announcer");
603
+ if (!a) return;
604
+ a.textContent = "";
605
+ // Re-asignar en el proximo tick asegura que el lector lo anuncie de nuevo.
606
+ window.setTimeout(function () { a.textContent = text; }, 30);
607
+ }
608
+
609
+ // ── Carga de datos ─────────────────────────────────────────────────────────
610
+ var warned = false;
611
+ function note(msg) {
612
+ if (!warned) { warned = true; console.warn("[leadership] " + msg); }
613
+ }
614
+
615
+ async function fetchJson(url) {
616
+ var res = await fetch(url + (url.indexOf("?") === -1 ? "?" : "&") + "_t=" + Date.now(), { cache: "no-store" });
617
+ if (!res.ok) throw new Error(url + " -> HTTP " + res.status);
618
+ return res.json();
619
+ }
620
+
621
+ async function loadSnapshot() {
622
+ // 1. Servidor con endpoint de snapshot (live: state + bus + artifacts).
623
+ try {
624
+ var live = await fetchJson(CONFIG.api);
625
+ if (live && live.state) return Object.assign({ mode: "live" }, live);
626
+ } catch (e) { note(String(e)); }
627
+
628
+ // 2. Servidor estatico desde la raiz del repo: solo state.json/roster.json.
629
+ try {
630
+ var pair = await Promise.all([fetchJson(CONFIG.stateFile), fetchJson(CONFIG.rosterFile).catch(function () { return null; })]);
631
+ if (pair[0] && pair[0].cards) {
632
+ return { mode: "files", state: pair[0], roster: pair[1] || {}, messages: [], generatedAt: new Date().toISOString() };
633
+ }
634
+ } catch (e) { note(String(e)); }
635
+
636
+ // 3. Snapshot estatico generado por scripts/snapshot.mjs (file:// amigable).
637
+ try {
638
+ var snap = await fetchJson(CONFIG.snapshotFile);
639
+ if (snap && snap.state) return Object.assign({ mode: "snapshot" }, snap);
640
+ } catch (e) { note(String(e)); }
641
+
642
+ // 4. Demo embebido: el panel nunca queda en blanco.
643
+ return Object.assign({ mode: "demo" }, L.demoSnapshot());
644
+ }
645
+
646
+ // ── Render ─────────────────────────────────────────────────────────────────
647
+ var MODE_LABEL = {
648
+ live: "live · .oficina",
649
+ files: "archivos · state.json",
650
+ snapshot: "snapshot estatico",
651
+ demo: "demo embebido",
652
+ };
653
+
654
+ function setMode(mode) {
655
+ state.mode = mode;
656
+ var badge = $("mode-badge");
657
+ badge.dataset.mode = mode;
658
+ $("mode-text").textContent = MODE_LABEL[mode] || mode;
659
+ $("source-note").textContent =
660
+ mode === "live" ? "api /api/snapshot" :
661
+ mode === "files" ? "../.oficina/state.json (sin bus)" :
662
+ mode === "snapshot" ? "dashboard/data/snapshot.json" :
663
+ "datos demo — corré node dashboard/scripts/serve.mjs para datos reales";
664
+ }
665
+
666
+ function stampNode(item) {
667
+ return el("span", { class: "stamp", attrs: { title: item.at } }, [
668
+ el("time", { text: L.formatClock(item.at), attrs: { datetime: item.at } }),
669
+ el("span", { class: "rel", text: "· " + L.formatRelative(item.at) }),
670
+ ]);
671
+ }
672
+
673
+ function cardChip(item) {
674
+ if (!item.cardId) return el("span", { class: "no-card", text: "sin card (global)" });
675
+ return el("button", {
676
+ class: "card-link",
677
+ type: "button",
678
+ text: item.cardId,
679
+ attrs: { title: "Ver card " + item.cardId + " y sus artifacts" },
680
+ onclick: function (ev) { ev.stopPropagation(); openCard(item.cardId); },
681
+ });
682
+ }
683
+
684
+ function decisionNode(item, index) {
685
+ var article = el("li", { class: "decision", dataset: { priority: item.priority, id: item.id, index: String(index) } }, [
686
+ el("div", { class: "dec-top" }, [
687
+ stampNode(item),
688
+ el("span", { class: "chip actor" }, [
689
+ el("span", { text: item.actorLabel }),
690
+ el("span", { class: "role", text: " · " + item.actorRole }),
691
+ ]),
692
+ cardChip(item),
693
+ el("span", { class: "chip type", text: item.typeLabel }),
694
+ item.channel ? el("span", { class: "chip", text: item.channel }) : null,
695
+ item.priority === "critical" ? el("span", { class: "chip prio-critical", text: "critical" }) :
696
+ item.priority === "high" ? el("span", { class: "chip prio-high", text: "high" }) : null,
697
+ ]),
698
+ el("h3", { class: "dec-title", text: item.decision }),
699
+ el("dl", { class: "dec-field" }, [
700
+ el("dt", { text: "Razón" }),
701
+ el("dd", { text: item.reason }),
702
+ el("dt", { text: "Card" }),
703
+ el("dd", { class: "mono", text: item.cardId ? (item.cardId + (item.cardTitle ? " — " + item.cardTitle : "")) + (item.cardState ? " (" + item.cardState + ")" : "") : "—" }),
704
+ ]),
705
+ el("dl", { class: "next" }, [
706
+ el("dt", { text: "Siguiente" }),
707
+ el("dd", { text: item.nextAction }),
708
+ ]),
709
+ el("div", { class: "dec-links" }, [
710
+ item.cardId ? el("a", { href: item.links.artifacts, text: "artifacts/" + item.cardId, attrs: { title: "Carpeta de artifacts de la card" } }) : null,
711
+ item.cardId ? el("a", { href: item.links.handoff, text: "handoff.md" }) : null,
712
+ item.cardId ? el("a", { href: item.links.evidence, text: "evidence.md" }) : null,
713
+ item.cardId ? el("a", { href: item.links.kanban, text: "kanban ↗", attrs: { title: "Abrir la card en el Kanban (T-07)" } }) : null,
714
+ item.cardId ? el("button", { class: "linkish", type: "button", text: "ver detalle", onclick: function (ev) { ev.stopPropagation(); openCard(item.cardId); } }) : null,
715
+ ]),
716
+ ]);
717
+ article.setAttribute("tabindex", "-1");
718
+ article.setAttribute("aria-label",
719
+ item.typeLabel + " por " + item.actorLabel + (item.cardId ? " sobre " + item.cardId : "") + ": " + item.decision);
720
+ article.addEventListener("keydown", function (ev) {
721
+ if (ev.key === "Enter" && item.cardId) { ev.preventDefault(); openCard(item.cardId); }
722
+ });
723
+ return article;
724
+ }
725
+
726
+ function renderSection(coordinator, list) {
727
+ var ul = $("list-" + coordinator);
728
+ var count = $("count-" + coordinator);
729
+ ul.textContent = "";
730
+ count.textContent = String(list.length);
731
+ if (!list.length) {
732
+ ul.appendChild(el("li", {}, [el("p", { class: "empty", text: "Sin decisiones con los filtros actuales." })]));
733
+ return;
734
+ }
735
+ list.forEach(function (item, i) { ul.appendChild(decisionNode(item, i)); });
736
+ }
737
+
738
+ function renderCards() {
739
+ var opts = L.filterOptions(state.items);
740
+ var select = $("f-card");
741
+ var current = state.filters.card;
742
+ select.textContent = "";
743
+ select.appendChild(el("option", { value: "all", text: "Todas" }));
744
+ opts.cards.forEach(function (c) {
745
+ select.appendChild(el("option", { value: c.id, text: c.id + (c.title ? " — " + L.clip(c.title, 34) : "") }));
746
+ });
747
+ if (current !== "all" && !opts.cards.some(function (c) { return c.id === current; })) current = "all";
748
+ select.value = current;
749
+ state.filters.card = current;
750
+ }
751
+
752
+ function renderTypes() {
753
+ var opts = L.filterOptions(state.items);
754
+ var select = $("f-type");
755
+ var current = state.filters.type;
756
+ select.textContent = "";
757
+ select.appendChild(el("option", { value: "all", text: "Todos" }));
758
+ opts.types.forEach(function (t) {
759
+ select.appendChild(el("option", { value: t.id, text: t.label + " · " + t.coordinator + " (" + t.count + ")" }));
760
+ });
761
+ if (current !== "all" && !opts.types.some(function (t) { return t.id === current; })) current = "all";
762
+ select.value = current;
763
+ state.filters.type = current;
764
+ }
765
+
766
+ function render() {
767
+ var filtered = L.filterDecisions(state.items, state.filters);
768
+ var grouped = L.groupByCoordinator(filtered);
769
+ ["commander", "integrator", "architect"].forEach(function (c) { renderSection(c, grouped[c]); });
770
+
771
+ $("counter").textContent = filtered.length + " de " + state.items.length;
772
+ state.view = grouped;
773
+
774
+ var total = state.items.length;
775
+ if (!state.firstLoad && total > state.lastTotal) {
776
+ announce((total - state.lastTotal) + " decisiones nuevas en el panel del jefe");
777
+ }
778
+ state.lastTotal = total;
779
+ state.firstLoad = false;
780
+ state.lastRenderSig = sig();
781
+ }
782
+
783
+ /** Firma de datos+filtros: evita re-render si nada cambio (mantiene el foco). */
784
+ function sig() {
785
+ return JSON.stringify({
786
+ f: state.filters,
787
+ i: state.items.map(function (x) { return x.id + "@" + x.at + ":" + x.cardState; }),
788
+ });
789
+ }
790
+
791
+ function renderAll(force) {
792
+ renderCards();
793
+ renderTypes();
794
+ var next = sig();
795
+ if (force || next !== state.lastRenderSig) render();
796
+ $("last-update").textContent = "actualizado " + new Date().toLocaleTimeString();
797
+ }
798
+
799
+ // ── Detalle de card ────────────────────────────────────────────────────────
800
+ function openCard(cardId) {
801
+ var state0 = (state.snapshot && state.snapshot.state) || {};
802
+ var card = state0.cards && state0.cards[cardId] ? state0.cards[cardId] : null;
803
+ var links = L.cardLinks(cardId);
804
+ var body = $("dlg-card-body");
805
+ body.textContent = "";
806
+ $("dlg-card-title").textContent = "Card " + cardId;
807
+
808
+ var related = state.items.filter(function (i) { return i.cardId === cardId; });
809
+
810
+ body.appendChild(el("dl", { class: "kv" }, [
811
+ el("dt", { text: "Titulo" }),
812
+ el("dd", { text: card && card.title ? card.title : "(no esta en state.json)" }),
813
+ el("dt", { text: "Estado" }),
814
+ el("dd", {}, [el("span", { class: "state-badge", dataset: { state: (card && card.state) || "unknown" }, text: (card && card.state) || "desconocido" })]),
815
+ el("dt", { text: "Owner" }),
816
+ el("dd", { class: "mono", text: (card && card.owner) || "sin owner" }),
817
+ el("dt", { text: "Reviewer" }),
818
+ el("dd", { class: "mono", text: (card && card.reviewer) || "sin reviewer" }),
819
+ el("dt", { text: "Intentos" }),
820
+ el("dd", { text: card ? (card.attempts || 0) + " intento(s) · " + (card.rejections || 0) + " rechazo(s)" : "—" }),
821
+ el("dt", { text: "Gate" }),
822
+ el("dd", { class: "mono", text: (card && card.gate) || "—" }),
823
+ ]));
824
+
825
+ var acc = card && Array.isArray(card.acceptance) ? card.acceptance : [];
826
+ body.appendChild(el("div", {}, [
827
+ el("div", { class: "kv" }, [el("dt", { text: "Criterios" })]),
828
+ acc.length
829
+ ? el("ul", { class: "clean" }, acc.map(function (a) { return el("li", { text: a }); }))
830
+ : el("p", { class: "hint", text: "Sin criterios de aceptacion en state.json." }),
831
+ ]));
832
+
833
+ body.appendChild(el("div", {}, [
834
+ el("div", { class: "kv" }, [el("dt", { text: "Artifacts" })]),
835
+ el("div", { class: "linkrow" }, [
836
+ el("a", { href: links.artifacts, text: "carpeta " + cardId + "/" }),
837
+ el("a", { href: links.handoff, text: "handoff.md" }),
838
+ el("a", { href: links.evidence, text: "evidence.md" }),
839
+ el("a", { href: links.gates, text: "gates.md" }),
840
+ el("a", { href: links.kanban, text: "ver en Kanban ↗" }),
841
+ ]),
842
+ ]));
843
+
844
+ body.appendChild(el("div", {}, [
845
+ el("div", { class: "kv" }, [el("dt", { text: "Decisiones" })]),
846
+ related.length
847
+ ? el("ul", { class: "clean" }, related.slice(0, 8).map(function (r) {
848
+ return el("li", { text: L.formatRelative(r.at) + " · " + r.actorLabel + " · " + r.decision });
849
+ }))
850
+ : el("p", { class: "hint", text: "Esta card no tiene decisiones en el panel." }),
851
+ ]));
852
+
853
+ var dlg = $("dlg-card");
854
+ if (typeof dlg.showModal === "function") dlg.showModal(); else dlg.setAttribute("open", "");
855
+ }
856
+
857
+ function openHelp() {
858
+ var dlg = $("dlg-help");
859
+ if (typeof dlg.showModal === "function") dlg.showModal(); else dlg.setAttribute("open", "");
860
+ }
861
+
862
+ // ── Polling ────────────────────────────────────────────────────────────────
863
+ async function tick(force) {
864
+ var snap;
865
+ try {
866
+ snap = await loadSnapshot();
867
+ } catch (e) {
868
+ console.error("[leadership] fallo de carga", e);
869
+ return;
870
+ }
871
+ state.snapshot = snap;
872
+ setMode(snap.mode || "snapshot");
873
+ var runId = (snap.state && snap.state.runId) || "?";
874
+ var runStatus = (snap.state && snap.state.status) || "?";
875
+ var objective = (snap.state && snap.state.objective) || "";
876
+ $("runline").textContent = "run " + runId + " · " + runStatus + (objective ? " · " + L.clip(objective, 70) : "");
877
+
878
+ var extracted = L.extractDecisions(snap, { now: Date.now() });
879
+ state.items = extracted.items;
880
+ renderAll(!!force);
881
+
882
+ // En modos no-live no tiene sentido seguir golpeando (snapshot/demo son estaticos).
883
+ if ((snap.mode === "snapshot" || snap.mode === "demo") && state.timer) {
884
+ window.clearInterval(state.timer);
885
+ state.timer = null;
886
+ $("btn-pause").setAttribute("aria-pressed", "false");
887
+ $("btn-pause").textContent = "⏸ Pausar";
888
+ }
889
+ }
890
+
891
+ function startPolling() {
892
+ stopPolling();
893
+ state.timer = window.setInterval(function () {
894
+ if (state.paused) return;
895
+ if (document.hidden) return; // no gastar ciclos en pestaña oculta
896
+ tick(false);
897
+ }, CONFIG.pollMs);
898
+ }
899
+ function stopPolling() {
900
+ if (state.timer) { window.clearInterval(state.timer); state.timer = null; }
901
+ }
902
+ function setPaused(paused) {
903
+ state.paused = paused;
904
+ var b = $("btn-pause");
905
+ b.setAttribute("aria-pressed", paused ? "true" : "false");
906
+ b.textContent = paused ? "▶ Reanudar" : "⏸ Pausar";
907
+ $("mode-badge").dataset.paused = String(paused);
908
+ announce(paused ? "Polling pausado" : "Polling reanudado");
909
+ }
910
+
911
+ // ── Navegacion por teclado entre decisiones ────────────────────────────────
912
+ function focusableDecisions() {
913
+ return Array.prototype.slice.call(document.querySelectorAll("#board li.decision"));
914
+ }
915
+
916
+ function moveFocus(delta) {
917
+ var list = focusableDecisions();
918
+ if (!list.length) return;
919
+ var active = document.activeElement;
920
+ var idx = list.indexOf(active);
921
+ var next = idx === -1 ? (delta > 0 ? 0 : list.length - 1) : Math.min(list.length - 1, Math.max(0, idx + delta));
922
+ list[next].setAttribute("tabindex", "0");
923
+ list[next].focus();
924
+ }
925
+
926
+ function jumpToSection(n) {
927
+ var sections = document.querySelectorAll(".section");
928
+ var s = sections[n - 1];
929
+ if (!s) return;
930
+ var target = s.querySelector("li.decision") || s;
931
+ s.scrollIntoView({ block: "start", behavior: "smooth" });
932
+ target.focus();
933
+ announce("Seccion " + s.getAttribute("data-coordinator"));
934
+ }
935
+
936
+ // ── Wiring ─────────────────────────────────────────────────────────────────
937
+ function wire() {
938
+ $("f-coordinator").addEventListener("change", function (e) { state.filters.coordinator = e.target.value; renderAll(true); });
939
+ $("f-card").addEventListener("change", function (e) { state.filters.card = e.target.value; renderAll(true); });
940
+ $("f-type").addEventListener("change", function (e) { state.filters.type = e.target.value; renderAll(true); });
941
+
942
+ var q = $("f-query");
943
+ var debounce = null;
944
+ q.addEventListener("input", function (e) {
945
+ window.clearTimeout(debounce);
946
+ var v = e.target.value;
947
+ debounce = window.setTimeout(function () { state.filters.query = v; renderAll(true); }, 120);
948
+ });
949
+
950
+ $("f-only-decisions").addEventListener("change", function (e) {
951
+ state.filters.onlyDecisions = e.target.checked; renderAll(true);
952
+ });
953
+
954
+ $("btn-pause").addEventListener("click", function () { setPaused(!state.paused); });
955
+ $("btn-refresh").addEventListener("click", function () { tick(true); });
956
+ $("btn-theme").addEventListener("click", toggleTheme);
957
+ $("btn-help").addEventListener("click", openHelp);
958
+ $("dlg-card-close").addEventListener("click", function () { $("dlg-card").close(); });
959
+ $("dlg-help-close").addEventListener("click", function () { $("dlg-help").close(); });
960
+
961
+ document.addEventListener("keydown", function (ev) {
962
+ var tag = (ev.target && ev.target.tagName) || "";
963
+ var typing = tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA" || (ev.target && ev.target.isContentEditable);
964
+ if (ev.key === "Escape") {
965
+ ["dlg-card", "dlg-help"].forEach(function (id) {
966
+ var d = $(id);
967
+ if (d && d.open) d.close();
968
+ });
969
+ if (typing && ev.target === q) {
970
+ if (q.value) { q.value = ""; state.filters.query = ""; renderAll(true); }
971
+ else q.blur();
972
+ }
973
+ return;
974
+ }
975
+ if (typing) return;
976
+ if (ev.metaKey || ev.ctrlKey || ev.altKey) return;
977
+
978
+ switch (ev.key) {
979
+ case "j": ev.preventDefault(); moveFocus(1); break;
980
+ case "k": ev.preventDefault(); moveFocus(-1); break;
981
+ case "/": ev.preventDefault(); q.focus(); q.select && q.select(); break;
982
+ case " ": ev.preventDefault(); setPaused(!state.paused); break;
983
+ case "r": ev.preventDefault(); tick(true); announce("Datos refrescados"); break;
984
+ case "t": ev.preventDefault(); toggleTheme(); break;
985
+ case "?": ev.preventDefault(); openHelp(); break;
986
+ case "1": ev.preventDefault(); jumpToSection(1); break;
987
+ case "2": ev.preventDefault(); jumpToSection(2); break;
988
+ case "3": ev.preventDefault(); jumpToSection(3); break;
989
+ default: break;
990
+ }
991
+ });
992
+
993
+ // Volver a la pestaña = ponerse al dia de una.
994
+ document.addEventListener("visibilitychange", function () {
995
+ if (!document.hidden && !state.paused) tick(true);
996
+ });
997
+ }
998
+
999
+ // ── Arranque ───────────────────────────────────────────────────────────────
1000
+ initTheme();
1001
+ wire();
1002
+ tick(true).then(function () {
1003
+ startPolling();
1004
+ });
1005
+ })();
1006
+ </script>
1007
+ </body>
1008
+ </html>