@nac3/forge-cli 1.19.5 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/yf.d.ts.map +1 -1
- package/dist/bin/yf.js +2 -1
- package/dist/bin/yf.js.map +1 -1
- package/dist/chat/instance_lock.d.ts +98 -0
- package/dist/chat/instance_lock.d.ts.map +1 -1
- package/dist/chat/instance_lock.js +135 -11
- package/dist/chat/instance_lock.js.map +1 -1
- package/dist/chat/instances.d.ts +84 -0
- package/dist/chat/instances.d.ts.map +1 -0
- package/dist/chat/instances.js +224 -0
- package/dist/chat/instances.js.map +1 -0
- package/dist/chat/panel.d.ts.map +1 -1
- package/dist/chat/panel.js +276 -5
- package/dist/chat/panel.js.map +1 -1
- package/dist/chat/server.d.ts.map +1 -1
- package/dist/chat/server.js +64 -1
- package/dist/chat/server.js.map +1 -1
- package/dist/chat/tools/evidence.d.ts.map +1 -1
- package/dist/chat/tools/evidence.js +7 -3
- package/dist/chat/tools/evidence.js.map +1 -1
- package/dist/commands/task.d.ts +13 -0
- package/dist/commands/task.d.ts.map +1 -1
- package/dist/commands/task.js +134 -0
- package/dist/commands/task.js.map +1 -1
- package/dist/commands/upgrade.d.ts +15 -0
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +63 -0
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/commands/urazuke.d.ts.map +1 -1
- package/dist/commands/urazuke.js +68 -1
- package/dist/commands/urazuke.js.map +1 -1
- package/dist/core/forge_profile.d.ts.map +1 -1
- package/dist/core/forge_profile.js +41 -4
- package/dist/core/forge_profile.js.map +1 -1
- package/dist/core/whats_new.d.ts.map +1 -1
- package/dist/core/whats_new.js +14 -0
- package/dist/core/whats_new.js.map +1 -1
- package/dist/urazuke/current.d.ts +5 -0
- package/dist/urazuke/current.d.ts.map +1 -1
- package/dist/urazuke/current.js +14 -3
- package/dist/urazuke/current.js.map +1 -1
- package/dist/urazuke/cycle.d.ts.map +1 -1
- package/dist/urazuke/cycle.js +6 -2
- package/dist/urazuke/cycle.js.map +1 -1
- package/dist/urazuke/evidence.d.ts.map +1 -1
- package/dist/urazuke/evidence.js +9 -1
- package/dist/urazuke/evidence.js.map +1 -1
- package/dist/urazuke/freeze.d.ts +29 -0
- package/dist/urazuke/freeze.d.ts.map +1 -0
- package/dist/urazuke/freeze.js +150 -0
- package/dist/urazuke/freeze.js.map +1 -0
- package/dist/urazuke/gate.d.ts +18 -1
- package/dist/urazuke/gate.d.ts.map +1 -1
- package/dist/urazuke/gate.js +21 -0
- package/dist/urazuke/gate.js.map +1 -1
- package/dist/urazuke/gates_data.d.ts +219 -0
- package/dist/urazuke/gates_data.d.ts.map +1 -0
- package/dist/urazuke/gates_data.js +441 -0
- package/dist/urazuke/gates_data.js.map +1 -0
- package/dist/urazuke/prompts_client.d.ts +6 -0
- package/dist/urazuke/prompts_client.d.ts.map +1 -1
- package/dist/urazuke/prompts_client.js +1 -1
- package/dist/urazuke/prompts_client.js.map +1 -1
- package/dist/urazuke/receipts.d.ts +53 -1
- package/dist/urazuke/receipts.d.ts.map +1 -1
- package/dist/urazuke/receipts.js +65 -3
- package/dist/urazuke/receipts.js.map +1 -1
- package/dist/urazuke/ruleset.d.ts +56 -0
- package/dist/urazuke/ruleset.d.ts.map +1 -0
- package/dist/urazuke/ruleset.js +159 -0
- package/dist/urazuke/ruleset.js.map +1 -0
- package/dist/urazuke/start.d.ts +5 -0
- package/dist/urazuke/start.d.ts.map +1 -1
- package/dist/urazuke/start.js +39 -2
- package/dist/urazuke/start.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LIVE FORGE INSTANCES -- the register that did not exist (PND-161, PND-162,
|
|
3
|
+
* PND-160).
|
|
4
|
+
*
|
|
5
|
+
* `src/tasks/alive.ts` answers "what background TASK is still running" very
|
|
6
|
+
* well: the supervisor knows what it launched. But a chat panel and a
|
|
7
|
+
* `yf start` are not supervisor tasks. Nobody counted them, nobody warned
|
|
8
|
+
* about them, and no command could stop one. The owner found out the way
|
|
9
|
+
* you find out about this class of bug: on 2026-10-21 his machine had nine
|
|
10
|
+
* node processes holding 134 GB of virtual space between them, three of them
|
|
11
|
+
* launched two days earlier, and Windows declared virtual memory exhaustion
|
|
12
|
+
* seven times in eighteen minutes. The identity of the processes being
|
|
13
|
+
* killed was lost with their pids, because Forge kept no register.
|
|
14
|
+
*
|
|
15
|
+
* This module is the same lesson as alive.ts, one level up -- what does not
|
|
16
|
+
* finish has to be VISIBLE, COUNTABLE and STOPPABLE -- applied to the
|
|
17
|
+
* instances themselves. It reads the lock files (the only record an instance
|
|
18
|
+
* leaves) and answers three questions:
|
|
19
|
+
*
|
|
20
|
+
* 1. what is running now, since when, and how much memory does it hold
|
|
21
|
+
* (PND-162: `yf task list` and the panel's startup notice);
|
|
22
|
+
* 2. which previous session died without closing, and at what time
|
|
23
|
+
* (PND-161: the chat notice, dated by the lock's last heartbeat);
|
|
24
|
+
* 3. is anything running from the GLOBAL module right now
|
|
25
|
+
* (PND-160: the global update has to refuse, not warn-and-proceed).
|
|
26
|
+
*
|
|
27
|
+
* Virtual size, not just heap. A node process paged out entirely shows a
|
|
28
|
+
* working set of 0 MB and still counts its full commit charge against the
|
|
29
|
+
* system: that is what trips Windows event 2004, and it is why the heap
|
|
30
|
+
* number alone made the machine look healthy. The heartbeat cannot afford a
|
|
31
|
+
* subprocess, so it records heap and RSS; this module adds the virtual size
|
|
32
|
+
* at LIST time, with one query for every pid at once, and says so when the
|
|
33
|
+
* platform will not give it.
|
|
34
|
+
*
|
|
35
|
+
* ASCII-only.
|
|
36
|
+
*/
|
|
37
|
+
import { execFile } from 'node:child_process';
|
|
38
|
+
import { promisify } from 'node:util';
|
|
39
|
+
import { listInstanceLocks } from './instance_lock.js';
|
|
40
|
+
const execFileP = promisify(execFile);
|
|
41
|
+
const MAX_LISTED = 8;
|
|
42
|
+
function toLive(h, now, processStart) {
|
|
43
|
+
return {
|
|
44
|
+
pid: h.pid, port: h.port, root: h.root,
|
|
45
|
+
kind: h.kind ?? 'desconocido',
|
|
46
|
+
...(h.version ? { version: h.version } : {}),
|
|
47
|
+
started_at: h.since,
|
|
48
|
+
age_minutes: Math.max(0, Math.round((now - h.since) / 60000)),
|
|
49
|
+
...(h.beat_at != null ? { beat_at: h.beat_at, beat_age_minutes: Math.max(0, Math.round((now - h.beat_at) / 60000)) } : {}),
|
|
50
|
+
...(h.heap_mb != null ? { heap_mb: h.heap_mb } : {}),
|
|
51
|
+
...(h.heap_limit_mb != null ? { heap_limit_mb: h.heap_limit_mb } : {}),
|
|
52
|
+
...(h.rss_mb != null ? { rss_mb: h.rss_mb } : {}),
|
|
53
|
+
...(h.module_dir ? { module_dir: h.module_dir } : {}),
|
|
54
|
+
from_previous_session: h.since < processStart,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Virtual size in MB per pid, best effort, ONE query for all of them.
|
|
58
|
+
*
|
|
59
|
+
* Windows: Get-Process, which is the only place VirtualMemorySize64 lives.
|
|
60
|
+
* Linux: /proc/<pid>/statm, field 1, in pages. Elsewhere: nothing, and the
|
|
61
|
+
* caller is told it is nothing rather than zero. */
|
|
62
|
+
export async function virtualSizes(pids) {
|
|
63
|
+
const bytes = new Map();
|
|
64
|
+
if (!pids.length)
|
|
65
|
+
return { bytes, unavailable: false };
|
|
66
|
+
try {
|
|
67
|
+
if (process.platform === 'win32') {
|
|
68
|
+
const script = 'Get-Process -Id ' + pids.join(',') + ' -ErrorAction SilentlyContinue'
|
|
69
|
+
+ ' | ForEach-Object { "$($_.Id) $($_.VirtualMemorySize64)" }';
|
|
70
|
+
const { stdout } = await execFileP('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], { encoding: 'utf8', windowsHide: true, timeout: 8000 });
|
|
71
|
+
for (const line of stdout.split('\n')) {
|
|
72
|
+
const m = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
|
|
73
|
+
if (m)
|
|
74
|
+
bytes.set(Number(m[1]), Number(m[2]));
|
|
75
|
+
}
|
|
76
|
+
return { bytes, unavailable: false };
|
|
77
|
+
}
|
|
78
|
+
if (process.platform === 'linux') {
|
|
79
|
+
const fs = await import('node:fs/promises');
|
|
80
|
+
const pageSize = 4096;
|
|
81
|
+
for (const pid of pids) {
|
|
82
|
+
try {
|
|
83
|
+
const statm = await fs.readFile('/proc/' + pid + '/statm', 'utf8');
|
|
84
|
+
const pages = Number(statm.trim().split(/\s+/)[0]);
|
|
85
|
+
if (Number.isFinite(pages))
|
|
86
|
+
bytes.set(pid, pages * pageSize);
|
|
87
|
+
}
|
|
88
|
+
catch { /* gone between the listing and here */ }
|
|
89
|
+
}
|
|
90
|
+
return { bytes, unavailable: false };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch { /* the query itself failed: report unavailable, never zero */ }
|
|
94
|
+
return { bytes, unavailable: true };
|
|
95
|
+
}
|
|
96
|
+
/** The instance report. `with_vsz` costs one subprocess, so it is opt-in:
|
|
97
|
+
* the panel's startup notice does not need it, `yf task list` does. */
|
|
98
|
+
export async function instanceReport(opts = {}) {
|
|
99
|
+
const now = opts.now ?? Date.now();
|
|
100
|
+
const processStart = opts.processStart ?? (Date.now() - Math.round(process.uptime() * 1000));
|
|
101
|
+
const { alive, dead } = listInstanceLocks();
|
|
102
|
+
const live = alive.map((h) => toLive(h, now, processStart));
|
|
103
|
+
if (!opts.with_vsz || !live.length)
|
|
104
|
+
return { alive: live, dead };
|
|
105
|
+
const { bytes, unavailable } = await virtualSizes(live.map((i) => i.pid));
|
|
106
|
+
for (const i of live) {
|
|
107
|
+
const b = bytes.get(i.pid);
|
|
108
|
+
if (b != null)
|
|
109
|
+
i.vsz_mb = Math.round(b / (1024 * 1024));
|
|
110
|
+
}
|
|
111
|
+
return { alive: live, dead, ...(unavailable ? { vsz_unavailable: true } : {}) };
|
|
112
|
+
}
|
|
113
|
+
function age(minutes) {
|
|
114
|
+
if (minutes < 60)
|
|
115
|
+
return minutes + ' min';
|
|
116
|
+
const h = Math.floor(minutes / 60);
|
|
117
|
+
return h < 24 ? h + ' h' : Math.floor(h / 24) + ' d';
|
|
118
|
+
}
|
|
119
|
+
function hhmm(ms) {
|
|
120
|
+
const d = new Date(ms);
|
|
121
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
122
|
+
return p(d.getHours()) + ':' + p(d.getMinutes());
|
|
123
|
+
}
|
|
124
|
+
/** The chat line for a PREVIOUS session that died without closing (PND-161).
|
|
125
|
+
*
|
|
126
|
+
* Empty when there is nothing to say -- and that is the hard rule here: an
|
|
127
|
+
* ordered shutdown removes its lock, so it reaches this function as
|
|
128
|
+
* nothing, and produces no message. A notice about the normal case becomes
|
|
129
|
+
* noise and then stops being read, which would cost exactly the signal
|
|
130
|
+
* this was built to give. */
|
|
131
|
+
export function renderDeathNotice(dead) {
|
|
132
|
+
if (!dead.length)
|
|
133
|
+
return '';
|
|
134
|
+
const lines = [];
|
|
135
|
+
for (const d of dead.slice(0, MAX_LISTED)) {
|
|
136
|
+
const what = d.kind === 'start' ? 'El servidor' : 'La sesion';
|
|
137
|
+
if (d.beat_at != null) {
|
|
138
|
+
const mins = Math.max(0, Math.round((d.beat_at - d.since) / 60000));
|
|
139
|
+
lines.push(' - ' + what + ' anterior (PID ' + d.pid + ') se corto cerca de las ' + hhmm(d.beat_at)
|
|
140
|
+
+ ' sin cierre ordenado, despues de ' + age(mins) + ' viva.'
|
|
141
|
+
+ (d.heap_mb != null && d.heap_limit_mb ? ' Ultimo latido: heap ' + d.heap_mb + '/' + d.heap_limit_mb + ' MB'
|
|
142
|
+
+ (d.rss_mb != null ? ', RSS ' + d.rss_mb + ' MB' : '') + '.' : '')
|
|
143
|
+
+ (d.memory_pressure ? ' El heap venia arriba del 85% del techo: la muerte es CONSISTENTE con falta de memoria (no es prueba: Forge no ve el kill).' : ''));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
/* A lock from a Forge without the heartbeat. The death is certain --
|
|
147
|
+
* the lock was never released -- but the hour is not, and inventing
|
|
148
|
+
* one from the file's timestamps would be the same guessing the
|
|
149
|
+
* heartbeat exists to replace. */
|
|
150
|
+
lines.push(' - ' + what + ' anterior (PID ' + d.pid + ') murio sin cierre ordenado. La hora exacta no quedo'
|
|
151
|
+
+ ' registrada: ese panel es de una version sin latido.');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (dead.length > MAX_LISTED)
|
|
155
|
+
lines.push(' ... y ' + (dead.length - MAX_LISTED) + ' mas');
|
|
156
|
+
return ['[sesion] ' + (dead.length === 1 ? 'La sesion anterior de este proyecto no cerro bien:' : dead.length + ' sesiones anteriores no cerraron bien:'),
|
|
157
|
+
...lines,
|
|
158
|
+
'Si estabas en medio de un turno, revisa el ultimo mensaje: lo que el cerebro no alcanzo a guardar no esta.'].join('\n');
|
|
159
|
+
}
|
|
160
|
+
/** The chat line for OTHER instances still running (PND-162). Only the ones
|
|
161
|
+
* from a previous session: the one the owner just opened is not news, and
|
|
162
|
+
* a second panel on another project is legitimate and current. */
|
|
163
|
+
export function renderOldInstancesNotice(alive, selfPid = process.pid) {
|
|
164
|
+
const old = alive.filter((i) => i.pid !== selfPid && i.from_previous_session);
|
|
165
|
+
if (!old.length)
|
|
166
|
+
return '';
|
|
167
|
+
const lines = [
|
|
168
|
+
'[instancias] Hay ' + old.length + ' proceso(s) de Forge de una sesion anterior todavia vivos:',
|
|
169
|
+
...old.slice(0, MAX_LISTED).map((i) => ' - PID ' + i.pid + ' [' + i.kind + ']'
|
|
170
|
+
+ ' hace ' + age(i.age_minutes)
|
|
171
|
+
+ (i.rss_mb != null ? ', RSS ' + i.rss_mb + ' MB' : '')
|
|
172
|
+
+ (i.vsz_mb != null ? ', virtual ' + i.vsz_mb + ' MB' : '')
|
|
173
|
+
+ ' -- ' + i.root),
|
|
174
|
+
];
|
|
175
|
+
if (old.length > MAX_LISTED)
|
|
176
|
+
lines.push(' ... y ' + (old.length - MAX_LISTED) + ' mas');
|
|
177
|
+
lines.push('Cada uno reserva memoria del sistema aunque no lo uses. Los listas con `yf task list` y los paras con `yf instance stop <pid>`.');
|
|
178
|
+
return lines.join('\n');
|
|
179
|
+
}
|
|
180
|
+
/** Stop a Forge instance by pid, with its whole process tree.
|
|
181
|
+
*
|
|
182
|
+
* Two guards, and both matter. It refuses our own pid, and it refuses any
|
|
183
|
+
* pid that is not in the lock registry -- so this is a way to stop Forge
|
|
184
|
+
* instances, never a general process killer reachable from the chat. On
|
|
185
|
+
* Windows the tree flag is not optional: a panel's real work happens in
|
|
186
|
+
* children, and killing only the parent leaves them holding the memory
|
|
187
|
+
* that was the problem in the first place. */
|
|
188
|
+
export async function stopInstance(pid, selfPid = process.pid) {
|
|
189
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
190
|
+
return { stopped: false, reason: 'pid invalido' };
|
|
191
|
+
if (pid === selfPid)
|
|
192
|
+
return { stopped: false, reason: 'es el proceso actual: cerralo normalmente' };
|
|
193
|
+
const { alive } = listInstanceLocks();
|
|
194
|
+
if (!alive.some((h) => h.pid === pid)) {
|
|
195
|
+
return { stopped: false, reason: 'no es una instancia de Forge viva (no hay lock con ese pid)' };
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
if (process.platform === 'win32') {
|
|
199
|
+
await execFileP('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true, timeout: 10000 });
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
process.kill(pid, 'SIGTERM');
|
|
203
|
+
}
|
|
204
|
+
return { stopped: true };
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
return { stopped: false, reason: e instanceof Error ? e.message.split('\n')[0] : String(e) };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Instances running from the GLOBAL module, for the update precheck
|
|
211
|
+
* (PND-160). A panel keeps better_sqlite3.node mapped, and on Windows a
|
|
212
|
+
* mapped file cannot be replaced -- so `npm i -g` deletes the old module,
|
|
213
|
+
* fails to write the new native binary, and aborts leaving the global
|
|
214
|
+
* package as a bare node_modules with no dist and no binaries. `yf` stops
|
|
215
|
+
* existing. The matching is on the lock's own record of where the process
|
|
216
|
+
* came from, so it does not depend on reading another process's argv. */
|
|
217
|
+
export function instancesFromGlobal(alive, globalPrefix, selfPid = process.pid) {
|
|
218
|
+
const norm = (p) => (process.platform === 'win32' ? p.toLowerCase() : p).replace(/[\\/]+$/, '');
|
|
219
|
+
const prefix = norm(globalPrefix);
|
|
220
|
+
if (!prefix)
|
|
221
|
+
return [];
|
|
222
|
+
return alive.filter((i) => i.pid !== selfPid && norm(i.module_dir ?? '').startsWith(prefix));
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=instances.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instances.js","sourceRoot":"","sources":["../../src/chat/instances.ts"],"names":[],"mappings":"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"}
|
package/dist/chat/panel.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/chat/panel.ts"],"names":[],"mappings":"AAmBA,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,kBAAkB,CAAC;AAG1B,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;2BAKuB;IACvB,IAAI,CAAC,EAAE,iBAAiB,CAAC;CAC1B;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/chat/panel.ts"],"names":[],"mappings":"AAmBA,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,kBAAkB,CAAC;AAG1B,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;2BAKuB;IACvB,IAAI,CAAC,EAAE,iBAAiB,CAAC;CAC1B;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAsqcxD"}
|
package/dist/chat/panel.js
CHANGED
|
@@ -1103,6 +1103,16 @@ body {
|
|
|
1103
1103
|
#yf-full .pz-pnd-chip.pnd-prio-cold { border-color: #d4cbb8; color: #8d8470; }
|
|
1104
1104
|
#yf-full .pz-pnd-chip.pnd-doc { border-color: rgba(111,139,94,0.6); color: #5d7a4c; }
|
|
1105
1105
|
#yf-full .pz-pnd-chip.pnd-nodoc { border-color: rgba(164,74,54,0.4); color: #a44a36; }
|
|
1106
|
+
/* PND-156: el reporte honesto es OTRO dato que el expediente, y antes
|
|
1107
|
+
* compartia la clase pnd-doc, con lo cual se veian identicos y parecian el
|
|
1108
|
+
* mismo indicador repetido. Indigo sumi-e, no verde. */
|
|
1109
|
+
#yf-full .pz-pnd-chip.pnd-report { border-color: rgba(79,91,135,0.6); color: #4f5b87; background: rgba(79,91,135,0.06); }
|
|
1110
|
+
/* PND-155: control de orden de la lista. */
|
|
1111
|
+
#yf-full .pz-pnd-sort { font: inherit; font-size: 11px; color: var(--ink-muted, #6f6650); background: var(--bg-0, #fffdf7);
|
|
1112
|
+
border: 1px solid #d4cbb8; border-radius: 4px; padding: 2px 4px; cursor: pointer; }
|
|
1113
|
+
#yf-full .pz-pnd-dir { font: inherit; font-size: 11px; width: 24px; color: var(--ink-muted, #6f6650);
|
|
1114
|
+
background: var(--bg-0, #fffdf7); border: 1px solid #d4cbb8; border-radius: 4px; cursor: pointer; padding: 2px 0; }
|
|
1115
|
+
#yf-full .pz-pnd-dir:hover, #yf-full .pz-pnd-sort:hover { border-color: var(--amber, #c8a04d); color: var(--ink, #3b3528); }
|
|
1106
1116
|
/* Pestana BPMN 2.0 (flujos de proceso). */
|
|
1107
1117
|
#yf-full .pz-body.pz-bpmn-wrap { background: var(--bg-1, #f5f0e6); padding: 12px; overflow: auto; }
|
|
1108
1118
|
#yf-full .pz-bpmn-svg { display: block; width: 100%; height: auto; }
|
|
@@ -3292,7 +3302,10 @@ let state = {
|
|
|
3292
3302
|
{ id, title, kind, content, language?, file_path?,
|
|
3293
3303
|
isEditing?, editValue?, dirty? }
|
|
3294
3304
|
activeId picks which tab body to render. */
|
|
3295
|
-
|
|
3305
|
+
/* PND-153: ui = memoria por pestana (foco, caret, scroll); renderedId =
|
|
3306
|
+
* la pestana que esta EN EL DOM, que en un cambio de pestana NO es la
|
|
3307
|
+
* activa -- confundir esas dos era el bug. */
|
|
3308
|
+
pizarron: { items: [], activeId: null, ui: {}, renderedId: null },
|
|
3296
3309
|
/* alpha.43 F1 -- cached NAC3 manifest + version. Updated on
|
|
3297
3310
|
boot via GET /api/forge/nac3-manifest and on every chat
|
|
3298
3311
|
response that carries nac3_manifest_version. Mismatch
|
|
@@ -5665,6 +5678,7 @@ function pizarronDropItem(id) {
|
|
|
5665
5678
|
const idx = state.pizarron.items.findIndex((x) => x.id === id);
|
|
5666
5679
|
if (idx < 0) return;
|
|
5667
5680
|
state.pizarron.items.splice(idx, 1);
|
|
5681
|
+
pzForgetUiState(id); /* PND-153: no acumular memoria de pestanas cerradas */
|
|
5668
5682
|
if (state.pizarron.activeId === id) {
|
|
5669
5683
|
const next = state.pizarron.items[state.pizarron.items.length - 1];
|
|
5670
5684
|
state.pizarron.activeId = next ? next.id : null;
|
|
@@ -5737,7 +5751,7 @@ function uzkRenderModal(r) {
|
|
|
5737
5751
|
for (var k = 0; k < r.receipts.length; k++) {
|
|
5738
5752
|
var rc = r.receipts[k];
|
|
5739
5753
|
h += '<li class="' + (rc.verdict === 'ok' ? 'uzk-ok' : 'uzk-fail') + '">' + escapeHtml(rc.id + ' ' + rc.kind + (rc.pending_id ? ' (' + rc.pending_id + ')' : '') + ' -- ' + (rc.verdict === 'ok' ? 'OK' : 'NOT OK'))
|
|
5740
|
-
+ '<span class="uzk-det">' + escapeHtml(rc.command + (rc.counts ? ' | ' + rc.counts : '')) + '</span></li>';
|
|
5754
|
+
+ '<span class="uzk-det">' + escapeHtml(rc.command + (rc.counts ? ' | ' + rc.counts : '') + (rc.why ? ' | ' + rc.why : '')) + '</span></li>';
|
|
5741
5755
|
}
|
|
5742
5756
|
h += '</ul>';
|
|
5743
5757
|
}
|
|
@@ -5847,6 +5861,7 @@ function pizarronCloseItem(id) {
|
|
|
5847
5861
|
yfCoding.raf = null;
|
|
5848
5862
|
}
|
|
5849
5863
|
state.pizarron.items.splice(idx, 1);
|
|
5864
|
+
pzForgetUiState(id); /* PND-153: no acumular memoria de pestanas cerradas */
|
|
5850
5865
|
if (state.pizarron.activeId === id) {
|
|
5851
5866
|
const next = state.pizarron.items[state.pizarron.items.length - 1];
|
|
5852
5867
|
state.pizarron.activeId = next ? next.id : null;
|
|
@@ -6268,14 +6283,146 @@ function pzRestoreScroll(body, snap) {
|
|
|
6268
6283
|
try { requestAnimationFrame(apply); } catch (e) { /* entorno sin rAF */ }
|
|
6269
6284
|
}
|
|
6270
6285
|
|
|
6286
|
+
/* ---- PND-153: memoria de UI POR PESTANA (foco, caret, scroll) ----
|
|
6287
|
+
*
|
|
6288
|
+
* Pablo lo planteo como impresion: "me parece que el foco es compartido
|
|
6289
|
+
* entre todas las pestanas, no guarda foco por pestana". Medido en el
|
|
6290
|
+
* codigo, es algo distinto y peor: el foco no se comparte, se DESTRUYE.
|
|
6291
|
+
* renderPizarron reescribe body.innerHTML entero en cada render, asi que
|
|
6292
|
+
* el elemento enfocado deja de existir y el navegador manda el foco al
|
|
6293
|
+
* body. Y el scroll solo se preservaba DENTRO de un mismo render de la
|
|
6294
|
+
* MISMA pestana (pzRestoreScroll sale temprano si cambio activeId), asi
|
|
6295
|
+
* que al volver a una pestana empezaba de cero.
|
|
6296
|
+
*
|
|
6297
|
+
* O sea: no habia estado de UI por pestana, ni de foco ni de scroll. Esto
|
|
6298
|
+
* lo agrega, con una regla: la memoria se guarda contra la pestana que
|
|
6299
|
+
* estaba EN EL DOM, no contra la activa -- en un cambio de pestana esas dos
|
|
6300
|
+
* son distintas, y escribirla contra la activa era el bug de origen. */
|
|
6301
|
+
|
|
6302
|
+
/** La clave con la que se vuelve a encontrar un elemento despues de que el
|
|
6303
|
+
* innerHTML lo destruyo. data-nac-id primero: la doctrina NAC-3 obliga a
|
|
6304
|
+
* que todo elemento interactivo lleve uno, y es estable entre renders.
|
|
6305
|
+
* Si no lo tiene, un camino tag+indice dentro de la tarjeta, que no es
|
|
6306
|
+
* estable si cambia el contenido pero si lo es en un re-render igual. */
|
|
6307
|
+
function pzFocusKey(el, root) {
|
|
6308
|
+
if (!el || !root || !root.contains(el)) return null;
|
|
6309
|
+
var nac = el.getAttribute && el.getAttribute('data-nac-id');
|
|
6310
|
+
/* Un id con comilla no se puede meter en un selector de atributo sin
|
|
6311
|
+
* escaparlo, y escapar aca es imposible: este bloque vive dentro de un
|
|
6312
|
+
* template literal de TypeScript, que se come las barras invertidas. Los
|
|
6313
|
+
* data-nac-id son puntos y alfanumericos por doctrina, asi que el caso no
|
|
6314
|
+
* existe -- y si apareciera, cae al camino estructural. */
|
|
6315
|
+
if (nac && nac.indexOf('"') < 0) return 'nac:' + nac;
|
|
6316
|
+
var parts = [];
|
|
6317
|
+
var cur = el;
|
|
6318
|
+
while (cur && cur !== root && cur.parentNode) {
|
|
6319
|
+
var sibs = cur.parentNode.children || [];
|
|
6320
|
+
var idx = 0;
|
|
6321
|
+
for (var i = 0; i < sibs.length; i++) { if (sibs[i] === cur) { idx = i; break; } }
|
|
6322
|
+
parts.unshift(cur.tagName + '[' + idx + ']');
|
|
6323
|
+
cur = cur.parentNode;
|
|
6324
|
+
}
|
|
6325
|
+
return parts.length ? 'path:' + parts.join('>') : null;
|
|
6326
|
+
}
|
|
6327
|
+
|
|
6328
|
+
/* Sin expresiones regulares y sin una sola barra invertida, a proposito:
|
|
6329
|
+
* este codigo se emite desde un template literal, asi que un \\[ o un \\d
|
|
6330
|
+
* en la fuente llegan al navegador como [ y d -- la primera version de esto
|
|
6331
|
+
* tenia /^(.+)\\[(\\d+)\\]$/ y el panel recibio /^(.+)[(d+)]$/, que casa
|
|
6332
|
+
* cualquier cosa y devolvia el elemento equivocado. El test lo agarro. */
|
|
6333
|
+
function pzFindByFocusKey(key, root) {
|
|
6334
|
+
if (!key || !root) return null;
|
|
6335
|
+
if (key.indexOf('nac:') === 0) {
|
|
6336
|
+
try { return root.querySelector('[data-nac-id="' + key.slice(4) + '"]'); } catch (e) { return null; }
|
|
6337
|
+
}
|
|
6338
|
+
if (key.indexOf('path:') !== 0) return null;
|
|
6339
|
+
var parts = key.slice(5).split('>');
|
|
6340
|
+
var cur = root;
|
|
6341
|
+
for (var i = 0; i < parts.length; i++) {
|
|
6342
|
+
var part = parts[i];
|
|
6343
|
+
var open = part.lastIndexOf('[');
|
|
6344
|
+
if (open <= 0 || part.charAt(part.length - 1) !== ']') return null;
|
|
6345
|
+
var tag = part.slice(0, open);
|
|
6346
|
+
var idx = Number(part.slice(open + 1, part.length - 1));
|
|
6347
|
+
if (!cur || !isFinite(idx)) return null;
|
|
6348
|
+
var kid = (cur.children || [])[idx];
|
|
6349
|
+
if (!kid || kid.tagName !== tag) return null;
|
|
6350
|
+
cur = kid;
|
|
6351
|
+
}
|
|
6352
|
+
return cur === root ? null : cur;
|
|
6353
|
+
}
|
|
6354
|
+
|
|
6355
|
+
/** Lo que el usuario estaba haciendo en la pestana que esta en el DOM. */
|
|
6356
|
+
function pzCaptureUiState(body) {
|
|
6357
|
+
if (!body) return;
|
|
6358
|
+
var tabId = state.pizarron.renderedId;
|
|
6359
|
+
if (!tabId) return;
|
|
6360
|
+
var entry = { };
|
|
6361
|
+
var sc = pzScroller(body);
|
|
6362
|
+
if (sc) {
|
|
6363
|
+
entry.top = sc.scrollTop;
|
|
6364
|
+
entry.atBottom = (sc.scrollHeight - sc.scrollTop - sc.clientHeight) <= PZ_STICK_PX;
|
|
6365
|
+
}
|
|
6366
|
+
try {
|
|
6367
|
+
var act = document.activeElement;
|
|
6368
|
+
var key = pzFocusKey(act, body);
|
|
6369
|
+
if (key) {
|
|
6370
|
+
entry.focusKey = key;
|
|
6371
|
+
/* El caret, no solo "el input estaba enfocado": volver al campo con
|
|
6372
|
+
* el cursor al principio es casi tan molesto como perder el foco. */
|
|
6373
|
+
if (typeof act.selectionStart === 'number') {
|
|
6374
|
+
entry.selStart = act.selectionStart;
|
|
6375
|
+
entry.selEnd = act.selectionEnd;
|
|
6376
|
+
}
|
|
6377
|
+
if (typeof act.scrollTop === 'number') entry.innerScroll = act.scrollTop;
|
|
6378
|
+
}
|
|
6379
|
+
} catch (e) { /* sin DOM de foco: nada que guardar */ }
|
|
6380
|
+
if (!state.pizarron.ui) state.pizarron.ui = {};
|
|
6381
|
+
state.pizarron.ui[tabId] = entry;
|
|
6382
|
+
}
|
|
6383
|
+
|
|
6384
|
+
/** Devolver al usuario donde estaba, en la pestana que se acaba de pintar. */
|
|
6385
|
+
function pzRestoreUiState(body) {
|
|
6386
|
+
var tabId = state.pizarron.activeId;
|
|
6387
|
+
if (!body || !tabId || !state.pizarron.ui) return;
|
|
6388
|
+
var entry = state.pizarron.ui[tabId];
|
|
6389
|
+
if (!entry) return;
|
|
6390
|
+
var apply = function () {
|
|
6391
|
+
var sc = pzScroller(body);
|
|
6392
|
+
if (sc && entry.top != null) sc.scrollTop = entry.atBottom ? sc.scrollHeight : entry.top;
|
|
6393
|
+
if (!entry.focusKey) return;
|
|
6394
|
+
var el = pzFindByFocusKey(entry.focusKey, body);
|
|
6395
|
+
if (!el || typeof el.focus !== 'function') return;
|
|
6396
|
+
/* preventScroll: el foco no debe pelearse con el scroll que acabamos
|
|
6397
|
+
* de restaurar dos lineas arriba. */
|
|
6398
|
+
try { el.focus({ preventScroll: true }); } catch (e) { try { el.focus(); } catch (e2) { return; } }
|
|
6399
|
+
if (entry.selStart != null && typeof el.setSelectionRange === 'function') {
|
|
6400
|
+
try { el.setSelectionRange(entry.selStart, entry.selEnd != null ? entry.selEnd : entry.selStart); } catch (e) { /* input sin seleccion */ }
|
|
6401
|
+
}
|
|
6402
|
+
if (entry.innerScroll != null && typeof el.scrollTop === 'number') el.scrollTop = entry.innerScroll;
|
|
6403
|
+
};
|
|
6404
|
+
apply();
|
|
6405
|
+
try { requestAnimationFrame(apply); } catch (e) { /* entorno sin rAF */ }
|
|
6406
|
+
}
|
|
6407
|
+
|
|
6408
|
+
/** Olvidar la memoria de una pestana cerrada: si no, el mapa crece con
|
|
6409
|
+
* cada pestana que el usuario abre y cierra durante la sesion. */
|
|
6410
|
+
function pzForgetUiState(id) {
|
|
6411
|
+
if (state.pizarron.ui && id in state.pizarron.ui) delete state.pizarron.ui[id];
|
|
6412
|
+
}
|
|
6413
|
+
|
|
6271
6414
|
function renderPizarron() {
|
|
6272
6415
|
const tabs = $('#pizarron-tabs');
|
|
6273
6416
|
const body = $('#pizarron-body');
|
|
6274
6417
|
const empty = $('#pizarron-empty');
|
|
6275
6418
|
const count = $('#pizarron-count');
|
|
6276
6419
|
if (!tabs || !body || !count) return;
|
|
6277
|
-
/* Antes de tocar el DOM: donde estaba mirando el usuario.
|
|
6420
|
+
/* Antes de tocar el DOM: donde estaba mirando el usuario. El snapshot
|
|
6421
|
+
* de scroll intra-render sigue (misma pestana); pzCaptureUiState guarda
|
|
6422
|
+
* ademas foco y caret CONTRA LA PESTANA QUE ESTA EN EL DOM, que es lo que
|
|
6423
|
+
* permite recuperarlos al volver (PND-153). */
|
|
6278
6424
|
const _pzScroll = pzCaptureScroll(body);
|
|
6425
|
+
pzCaptureUiState(body);
|
|
6279
6426
|
count.textContent = state.pizarron.items.length + ' ' + (state.pizarron.items.length === 1 ? CONFIG.i18n.tabOne : CONFIG.i18n.tabMany);
|
|
6280
6427
|
/* Render tabs */
|
|
6281
6428
|
applySavedTabOrder(); /* PND-137 -- respeta el orden elegido por drag */
|
|
@@ -6371,6 +6518,11 @@ function renderPizarron() {
|
|
|
6371
6518
|
}
|
|
6372
6519
|
/* Devolver al usuario donde estaba: pegado al fondo si venia pegado. */
|
|
6373
6520
|
pzRestoreScroll(body, _pzScroll);
|
|
6521
|
+
/* Y la memoria de ESTA pestana: foco, caret y scroll de la ultima vez que
|
|
6522
|
+
* el usuario estuvo en ella (PND-153). Va despues del scroll intra-render
|
|
6523
|
+
* para que gane el estado propio de la pestana al volver a ella. */
|
|
6524
|
+
state.pizarron.renderedId = state.pizarron.activeId;
|
|
6525
|
+
pzRestoreUiState(body);
|
|
6374
6526
|
}
|
|
6375
6527
|
|
|
6376
6528
|
/* Selector multi-workflow: persiste el indice elegido por tab id (igual
|
|
@@ -6507,6 +6659,21 @@ function renderPendingsView(content) {
|
|
|
6507
6659
|
+ '<input type="text" class="pz-pnd-q" data-pz-pnd-q spellcheck="false"'
|
|
6508
6660
|
+ ' placeholder="Buscar: id, titulo, archivo, caso de uso..."'
|
|
6509
6661
|
+ ' aria-label="Buscar en la lista" />'
|
|
6662
|
+
/* PND-155: tres criterios, los dos sentidos. "registro" es el orden que
|
|
6663
|
+
* manda el servidor (prioridad), y es el default: elegir orden es una
|
|
6664
|
+
* accion del usuario, no algo que le cambie la lista sin pedirlo. */
|
|
6665
|
+
+ '<select class="pz-pnd-sort" data-pz-pnd-sort'
|
|
6666
|
+
+ ' data-nac-id="yujin.pending.sort.criterion" data-nac-role="select"'
|
|
6667
|
+
+ ' data-nac-action="sort_pendings" aria-label="Ordenar la lista por">'
|
|
6668
|
+
+ ' <option value="default">orden del registro</option>'
|
|
6669
|
+
+ ' <option value="id">nro de PND</option>'
|
|
6670
|
+
+ ' <option value="title">titulo</option>'
|
|
6671
|
+
+ ' <option value="date">fecha de ' + (done ? 'cierre' : 'creacion') + '</option>'
|
|
6672
|
+
+ '</select>'
|
|
6673
|
+
+ '<button type="button" class="pz-pnd-dir" data-pz-pnd-dir'
|
|
6674
|
+
+ ' data-nac-id="yujin.pending.sort.direction" data-nac-role="action"'
|
|
6675
|
+
+ ' data-nac-action="toggle_sort_direction"'
|
|
6676
|
+
+ ' title="Invertir el orden" aria-label="Invertir el orden">↕</button>'
|
|
6510
6677
|
+ '<span class="pz-pnd-hint">click para ver el detalle</span>'
|
|
6511
6678
|
+ '</div>';
|
|
6512
6679
|
if (!d.items.length) {
|
|
@@ -6520,10 +6687,21 @@ function renderPendingsView(content) {
|
|
|
6520
6687
|
var meta = done
|
|
6521
6688
|
? '<span class="pz-pnd-when">' + escapeHtml(pndFmtDate(it.closed_at)) + '</span>'
|
|
6522
6689
|
+ '<span class="pz-pnd-chip pnd-reason">' + escapeHtml(pndReasonLabel(it.closed_reason)) + '</span>'
|
|
6690
|
+
/* PND-156. Pablo vio "un boton Reporte que hace lo mismo que el
|
|
6691
|
+
* boton doc". Medido: no son botones y no hacen lo mismo -- son dos
|
|
6692
|
+
* indicadores de DOS datos distintos (expediente de implementacion
|
|
6693
|
+
* vs reporte honesto), y parecian el mismo porque compartian la
|
|
6694
|
+
* clase pnd-doc, con lo cual se veian identicos, y porque el unico
|
|
6695
|
+
* elemento clickeable es la FILA entera: apretar sobre cualquiera
|
|
6696
|
+
* de los dos abre el detalle. Hacian lo mismo porque no hacian
|
|
6697
|
+
* nada. Esto es el cambio minimo: cada indicador con su estilo y su
|
|
6698
|
+
* texto propio, sin perder ninguno de los dos datos. Hacerlos
|
|
6699
|
+
* clickeables para que abran su seccion del detalle es una decision
|
|
6700
|
+
* de diseno de Pablo, y queda abierta. */
|
|
6523
6701
|
+ (impl && (impl.functional || impl.use_cases)
|
|
6524
|
-
? '<span class="pz-pnd-chip pnd-doc" title="tiene expediente de implementacion">
|
|
6702
|
+
? '<span class="pz-pnd-chip pnd-doc" title="tiene expediente de implementacion: que se puede hacer ahora y con que casos de uso">expediente</span>'
|
|
6525
6703
|
: '<span class="pz-pnd-chip pnd-nodoc" title="cerrado sin descripcion funcional">sin doc</span>')
|
|
6526
|
-
+ (impl && impl.report ? '<span class="pz-pnd-chip pnd-
|
|
6704
|
+
+ (impl && impl.report ? '<span class="pz-pnd-chip pnd-report" title="tiene reporte honesto: que quedo verificado, que creido y que es punto ciego">reporte</span>' : '')
|
|
6527
6705
|
/* PND-154 (Pablo 2026-10-20): la tarjeta de un pendiente ABIERTO
|
|
6528
6706
|
* muestra su fecha de CREACION, como la de Hecho ya mostraba la de
|
|
6529
6707
|
* cierre. El dato estaba en el registro desde siempre y no se veia,
|
|
@@ -6534,7 +6712,17 @@ function renderPendingsView(content) {
|
|
|
6534
6712
|
+ '<span class="pz-pnd-chip ' + pndPrioClass(it.priority) + '">' + escapeHtml(String(it.priority != null ? it.priority : '-')) + '</span>'
|
|
6535
6713
|
+ '<span class="pz-pnd-chip pnd-cat">' + escapeHtml(pndCatLabel(it.category)) + '</span>'
|
|
6536
6714
|
+ (it.blocker_who ? '<span class="pz-pnd-chip pnd-who">' + escapeHtml(String(it.blocker_who)) + '</span>' : '');
|
|
6715
|
+
/* PND-155: las claves de orden viajan en la fila, como data-pnd-q ya
|
|
6716
|
+
* hace para el buscador, asi que ordenar es reacomodar nodos en el DOM
|
|
6717
|
+
* -- sin request, sin re-render, y sin pelearse con el refresh de 12 s.
|
|
6718
|
+
* El numero va aparte del id a proposito: ordenar "PND-170" como texto
|
|
6719
|
+
* pone PND-10 antes de PND-9. */
|
|
6720
|
+
var pndNum = (function () { var m = /(\d+)\s*$/.exec(id); return m ? Number(m[1]) : -1; })();
|
|
6721
|
+
var pndTs = Number(done ? (it.closed_at || 0) : (it.created_at || 0)) || 0;
|
|
6537
6722
|
return '<button type="button" class="pz-pnd-row" data-pz-pnd-row data-pnd-id="' + escapeHtml(id) + '"'
|
|
6723
|
+
+ ' data-pnd-num="' + pndNum + '"'
|
|
6724
|
+
+ ' data-pnd-ts="' + pndTs + '"'
|
|
6725
|
+
+ ' data-pnd-title="' + escapeHtml(String((it && it.title) || '')) + '"'
|
|
6538
6726
|
+ ' data-pnd-q="' + escapeHtml(pendingSearchText(it)) + '"'
|
|
6539
6727
|
+ ' data-nac-id="yujin.pending.' + escapeHtml(id) + '.open"'
|
|
6540
6728
|
+ ' data-nac-role="action" data-nac-action="open_pending_detail">'
|
|
@@ -6704,6 +6892,61 @@ function pendingDetailMd(it, done) {
|
|
|
6704
6892
|
return L.join('\\n');
|
|
6705
6893
|
}
|
|
6706
6894
|
|
|
6895
|
+
/* ---- PND-155: orden de la lista de Pendientes / Hecho ----
|
|
6896
|
+
*
|
|
6897
|
+
* Tres criterios y los dos sentidos, con dos cuidados que el pedido nombra
|
|
6898
|
+
* explicitamente: el nro de PND se ordena NUMERICO (PND-9 antes de PND-10;
|
|
6899
|
+
* como texto sale al revas) y el titulo con localeCompare en el idioma
|
|
6900
|
+
* configurado, no byte a byte.
|
|
6901
|
+
*
|
|
6902
|
+
* Y uno que el pedido tambien nombra: la eleccion se guarda POR PESTANA
|
|
6903
|
+
* (Pendientes y Hecho son dos listas distintas y cada una tiene su orden
|
|
6904
|
+
* natural), no en una variable global -- que es el agujero de PND-153. */
|
|
6905
|
+
function pndSortKey(mode) {
|
|
6906
|
+
var slug = (CONFIG && (CONFIG.projectSlug || CONFIG.projectName)) || 'default';
|
|
6907
|
+
return 'yf_pnd_sort_' + slug + '_' + (mode || 'open');
|
|
6908
|
+
}
|
|
6909
|
+
|
|
6910
|
+
function pndReadSortPref(mode) {
|
|
6911
|
+
try {
|
|
6912
|
+
var raw = localStorage.getItem(pndSortKey(mode));
|
|
6913
|
+
if (raw) {
|
|
6914
|
+
var p = JSON.parse(raw);
|
|
6915
|
+
if (p && typeof p.by === 'string') return { by: p.by, dir: p.dir === 'desc' ? 'desc' : 'asc' };
|
|
6916
|
+
}
|
|
6917
|
+
} catch (e) { /* sin localStorage: el default alcanza */ }
|
|
6918
|
+
return { by: 'default', dir: 'asc' };
|
|
6919
|
+
}
|
|
6920
|
+
|
|
6921
|
+
function pndWriteSortPref(mode, pref) {
|
|
6922
|
+
try { localStorage.setItem(pndSortKey(mode), JSON.stringify(pref)); } catch (e) { /* no persiste, no importa */ }
|
|
6923
|
+
}
|
|
6924
|
+
|
|
6925
|
+
/** Reordenar las filas que ya estan en el DOM. rows es el orden ORIGINAL
|
|
6926
|
+
* del registro, que es lo que permite volver a 'default' sin recargar. */
|
|
6927
|
+
function pndApplySort(list, rows, pref) {
|
|
6928
|
+
if (!list) return;
|
|
6929
|
+
var arr = [];
|
|
6930
|
+
for (var i = 0; i < rows.length; i++) arr.push(rows[i]);
|
|
6931
|
+
if (pref.by !== 'default') {
|
|
6932
|
+
var lang = (CONFIG && CONFIG.lang) || undefined;
|
|
6933
|
+
var cmp = function (a, b) {
|
|
6934
|
+
if (pref.by === 'id') return Number(a.getAttribute('data-pnd-num')) - Number(b.getAttribute('data-pnd-num'));
|
|
6935
|
+
if (pref.by === 'date') return Number(a.getAttribute('data-pnd-ts')) - Number(b.getAttribute('data-pnd-ts'));
|
|
6936
|
+
var ta = a.getAttribute('data-pnd-title') || '';
|
|
6937
|
+
var tb = b.getAttribute('data-pnd-title') || '';
|
|
6938
|
+
try { return ta.localeCompare(tb, lang); } catch (e) { return ta < tb ? -1 : (ta > tb ? 1 : 0); }
|
|
6939
|
+
};
|
|
6940
|
+
arr.sort(cmp);
|
|
6941
|
+
if (pref.dir === 'desc') arr.reverse();
|
|
6942
|
+
} else if (pref.dir === 'desc') {
|
|
6943
|
+
arr.reverse();
|
|
6944
|
+
}
|
|
6945
|
+
/* appendChild mueve el nodo: no se clona nada, asi que los handlers y el
|
|
6946
|
+
* estado de display del filtro sobreviven al reorden. */
|
|
6947
|
+
for (var j = 0; j < arr.length; j++) list.appendChild(arr[j]);
|
|
6948
|
+
}
|
|
6949
|
+
|
|
6707
6950
|
function initPendingsTab(it) {
|
|
6708
6951
|
var body = document.querySelector('[data-nac-id="yujin.pizarron.item.' + it.id + '.body"]');
|
|
6709
6952
|
if (!body) return;
|
|
@@ -6740,6 +6983,33 @@ function initPendingsTab(it) {
|
|
|
6740
6983
|
if (ev.key === 'Escape') { q.value = ''; applyFilter(); }
|
|
6741
6984
|
});
|
|
6742
6985
|
}
|
|
6986
|
+
|
|
6987
|
+
/* PND-155: el orden. Se aplica al abrir la pestana (con lo que el usuario
|
|
6988
|
+
* eligio la ultima vez) y en cada cambio del control. */
|
|
6989
|
+
var sortSel = body.querySelector('[data-pz-pnd-sort]');
|
|
6990
|
+
var dirBtn = body.querySelector('[data-pz-pnd-dir]');
|
|
6991
|
+
var list = body.querySelector('.pz-pnd-list');
|
|
6992
|
+
if (sortSel && dirBtn && list) {
|
|
6993
|
+
var mode = d.mode === 'done' ? 'done' : 'open';
|
|
6994
|
+
var pref = pndReadSortPref(mode);
|
|
6995
|
+
var paint = function () {
|
|
6996
|
+
sortSel.value = pref.by;
|
|
6997
|
+
dirBtn.textContent = pref.dir === 'desc' ? '↓' : '↑';
|
|
6998
|
+
dirBtn.setAttribute('title', pref.dir === 'desc' ? 'Orden descendente (click para invertir)' : 'Orden ascendente (click para invertir)');
|
|
6999
|
+
pndApplySort(list, rows, pref);
|
|
7000
|
+
};
|
|
7001
|
+
sortSel.addEventListener('change', function () {
|
|
7002
|
+
pref = { by: String(sortSel.value || 'default'), dir: pref.dir };
|
|
7003
|
+
pndWriteSortPref(mode, pref);
|
|
7004
|
+
paint();
|
|
7005
|
+
});
|
|
7006
|
+
dirBtn.addEventListener('click', function () {
|
|
7007
|
+
pref = { by: pref.by, dir: pref.dir === 'desc' ? 'asc' : 'desc' };
|
|
7008
|
+
pndWriteSortPref(mode, pref);
|
|
7009
|
+
paint();
|
|
7010
|
+
});
|
|
7011
|
+
paint();
|
|
7012
|
+
}
|
|
6743
7013
|
for (var k = 0; k < rows.length; k++) {
|
|
6744
7014
|
rows[k].addEventListener('click', function () {
|
|
6745
7015
|
var pid = this.getAttribute('data-pnd-id');
|
|
@@ -7449,6 +7719,7 @@ function autoCloseCodingTab() {
|
|
|
7449
7719
|
if (idx < 0) return;
|
|
7450
7720
|
if (yfCoding.raf != null) { try { cancelAnimationFrame(yfCoding.raf); } catch (e) { /* */ } yfCoding.raf = null; }
|
|
7451
7721
|
state.pizarron.items.splice(idx, 1);
|
|
7722
|
+
pzForgetUiState(CODING_TAB_ID); /* PND-153: no acumular memoria de pestanas cerradas */
|
|
7452
7723
|
if (state.pizarron.activeId === CODING_TAB_ID) {
|
|
7453
7724
|
const next = state.pizarron.items[state.pizarron.items.length - 1];
|
|
7454
7725
|
state.pizarron.activeId = next ? next.id : null;
|