@shardworks/spider-apparatus 0.1.279 → 0.1.281
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardworks/spider-apparatus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.281",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,17 +22,17 @@
|
|
|
22
22
|
"hono": "^4.7.11",
|
|
23
23
|
"yaml": "^2.0.0",
|
|
24
24
|
"zod": "4.3.6",
|
|
25
|
-
"@shardworks/fabricator-apparatus": "0.1.
|
|
26
|
-
"@shardworks/
|
|
27
|
-
"@shardworks/
|
|
28
|
-
"@shardworks/
|
|
29
|
-
"@shardworks/
|
|
30
|
-
"@shardworks/loom-apparatus": "0.1.
|
|
31
|
-
"@shardworks/
|
|
25
|
+
"@shardworks/fabricator-apparatus": "0.1.281",
|
|
26
|
+
"@shardworks/stacks-apparatus": "0.1.281",
|
|
27
|
+
"@shardworks/tools-apparatus": "0.1.281",
|
|
28
|
+
"@shardworks/animator-apparatus": "0.1.281",
|
|
29
|
+
"@shardworks/codexes-apparatus": "0.1.281",
|
|
30
|
+
"@shardworks/loom-apparatus": "0.1.281",
|
|
31
|
+
"@shardworks/clerk-apparatus": "0.1.281"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "25.5.0",
|
|
35
|
-
"@shardworks/nexus-core": "0.1.
|
|
35
|
+
"@shardworks/nexus-core": "0.1.281"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"dist",
|
|
@@ -316,16 +316,25 @@ describe('feedback.js tag filter toolbar', () => {
|
|
|
316
316
|
// ── Deep-link URL state (?feedback=ID) ──────────────────────────────────
|
|
317
317
|
|
|
318
318
|
describe('feedback.js — deep-link URL state', () => {
|
|
319
|
-
it('
|
|
319
|
+
it('routes URL reads/writes through window.NexusUrl (no inline helpers)', () => {
|
|
320
|
+
// Inline currentUrlParams / updateUrl are gone (commission moix23w5).
|
|
321
|
+
assert.ok(
|
|
322
|
+
!/function\s+currentUrlParams\s*\(/.test(feedbackJs),
|
|
323
|
+
'inline currentUrlParams must not be redeclared',
|
|
324
|
+
);
|
|
325
|
+
assert.ok(
|
|
326
|
+
!/function\s+updateUrl\s*\(/.test(feedbackJs),
|
|
327
|
+
'inline updateUrl must not be redeclared',
|
|
328
|
+
);
|
|
320
329
|
assert.match(
|
|
321
330
|
feedbackJs,
|
|
322
|
-
/
|
|
323
|
-
'
|
|
331
|
+
/window\.NexusUrl\.read\(\)/,
|
|
332
|
+
'feedback.js should read URL state via window.NexusUrl.read',
|
|
324
333
|
);
|
|
325
334
|
assert.match(
|
|
326
335
|
feedbackJs,
|
|
327
|
-
/
|
|
328
|
-
'
|
|
336
|
+
/window\.NexusUrl\.update\(/,
|
|
337
|
+
'feedback.js should write URL state via window.NexusUrl.update',
|
|
329
338
|
);
|
|
330
339
|
});
|
|
331
340
|
|
|
@@ -336,21 +345,29 @@ describe('feedback.js — deep-link URL state', () => {
|
|
|
336
345
|
assert.ok(block, 'should find showDetail body');
|
|
337
346
|
assert.match(
|
|
338
347
|
block[0],
|
|
339
|
-
/
|
|
340
|
-
'showDetail pushes ?feedback=<currentRequest.id>
|
|
348
|
+
/window\.NexusUrl\.update\(\{\s*feedback:\s*currentRequest\.id\s*\}\s*,\s*\{\s*push:\s*true\s*\}\)/,
|
|
349
|
+
'showDetail pushes ?feedback=<currentRequest.id> via NexusUrl.update with push: true',
|
|
341
350
|
);
|
|
342
351
|
assert.match(block[0], /skipUrlPush/, 'showDetail accepts a skipUrlPush opt');
|
|
343
352
|
});
|
|
344
353
|
|
|
345
|
-
it('navigateToList clears ?feedback
|
|
354
|
+
it('navigateToList clears ?feedback (and ?tag=) via NexusUrl with push: true (D11/D12)', () => {
|
|
346
355
|
const block = feedbackJs.match(
|
|
347
356
|
/function navigateToList\((?:opts)?\)[\s\S]*?fetchList\(\);[\s\S]*?startPoll\(\);\s*\}/,
|
|
348
357
|
);
|
|
349
358
|
assert.ok(block, 'should find navigateToList body');
|
|
350
359
|
assert.match(
|
|
351
360
|
block[0],
|
|
352
|
-
/
|
|
353
|
-
'navigateToList
|
|
361
|
+
/window\.NexusUrl\.update\(\{[^}]*feedback:\s*null[^}]*\}\s*,\s*\{\s*push:\s*true\s*\}\)/,
|
|
362
|
+
'navigateToList must push a clean URL with feedback: null',
|
|
363
|
+
);
|
|
364
|
+
// D12 — closing the detail also clears any per-detail ?tag= keys
|
|
365
|
+
// via the omit-defaults rule. The same NexusUrl.update call drops
|
|
366
|
+
// both keys at once.
|
|
367
|
+
assert.match(
|
|
368
|
+
block[0],
|
|
369
|
+
/tag:\s*null/,
|
|
370
|
+
'navigateToList must also drop ?tag= keys when the detail closes (D12)',
|
|
354
371
|
);
|
|
355
372
|
assert.ok(
|
|
356
373
|
!/window\.history\.back\s*\(/.test(block[0]),
|
|
@@ -370,8 +387,8 @@ describe('feedback.js — deep-link URL state', () => {
|
|
|
370
387
|
assert.ok(block, 'should find popstate handler body');
|
|
371
388
|
assert.match(
|
|
372
389
|
block[0],
|
|
373
|
-
/
|
|
374
|
-
'popstate handler reads
|
|
390
|
+
/readUrlState\(\)/,
|
|
391
|
+
'popstate handler reads URL state via the central readUrlState helper',
|
|
375
392
|
);
|
|
376
393
|
assert.match(
|
|
377
394
|
block[0],
|
|
@@ -380,11 +397,11 @@ describe('feedback.js — deep-link URL state', () => {
|
|
|
380
397
|
);
|
|
381
398
|
});
|
|
382
399
|
|
|
383
|
-
it('init reads
|
|
400
|
+
it('init reads URL state and resolves the deep-link via showDetailById', () => {
|
|
384
401
|
assert.match(
|
|
385
402
|
feedbackJs,
|
|
386
|
-
/var initialFeedbackId\s*=\s*
|
|
387
|
-
'init
|
|
403
|
+
/var initialFeedbackId\s*=\s*readUrlState\(\)/,
|
|
404
|
+
'init calls readUrlState before fetching the list',
|
|
388
405
|
);
|
|
389
406
|
assert.match(
|
|
390
407
|
feedbackJs,
|
|
@@ -399,7 +416,7 @@ describe('feedback.js — deep-link URL state', () => {
|
|
|
399
416
|
);
|
|
400
417
|
assert.ok(block, 'should find renderFeedbackNotFound body');
|
|
401
418
|
assert.ok(
|
|
402
|
-
!/
|
|
419
|
+
!/NexusUrl\.update/.test(block[0]),
|
|
403
420
|
'renderFeedbackNotFound must not rewrite the URL',
|
|
404
421
|
);
|
|
405
422
|
assert.match(
|
|
@@ -408,4 +425,63 @@ describe('feedback.js — deep-link URL state', () => {
|
|
|
408
425
|
'renderFeedbackNotFound surfaces a "not found" message',
|
|
409
426
|
);
|
|
410
427
|
});
|
|
428
|
+
|
|
429
|
+
it('list-page status filter writes through NexusUrl.update (replace, no push: true)', () => {
|
|
430
|
+
const writer = feedbackJs.match(
|
|
431
|
+
/function writeStatusFilterToUrl\(\)[\s\S]*?(?=\n function )/,
|
|
432
|
+
);
|
|
433
|
+
assert.ok(writer, 'writeStatusFilterToUrl should be defined');
|
|
434
|
+
assert.match(
|
|
435
|
+
writer[0],
|
|
436
|
+
/window\.NexusUrl\.update\(\{\s*status:[\s\S]*?\}\s*\)/,
|
|
437
|
+
'status filter must call NexusUrl.update without { push: true }',
|
|
438
|
+
);
|
|
439
|
+
assert.ok(
|
|
440
|
+
!/push:\s*true/.test(writer[0]),
|
|
441
|
+
'status filter writes must use replaceState (D5 default)',
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('per-detail tag filter writes ?tag= via repeated keys (D12 + D3)', () => {
|
|
446
|
+
const writer = feedbackJs.match(
|
|
447
|
+
/function writeTagFilterToUrl\(\)[\s\S]*?(?=\n function )/,
|
|
448
|
+
);
|
|
449
|
+
assert.ok(writer, 'writeTagFilterToUrl should be defined');
|
|
450
|
+
assert.match(
|
|
451
|
+
writer[0],
|
|
452
|
+
/Object\.keys\(activeTagFilters\)/,
|
|
453
|
+
'tag filter writer reads from activeTagFilters',
|
|
454
|
+
);
|
|
455
|
+
assert.match(
|
|
456
|
+
writer[0],
|
|
457
|
+
/window\.NexusUrl\.update\(\{\s*tag:[\s\S]*?\}\s*\)/,
|
|
458
|
+
'tag filter writer calls NexusUrl.update with the tag key (no push: true)',
|
|
459
|
+
);
|
|
460
|
+
assert.ok(
|
|
461
|
+
!/push:\s*true/.test(writer[0]),
|
|
462
|
+
'tag filter changes must use replaceState (D5 default)',
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('readUrlState validates ?status= against STATUS_VALUES and surfaces fail-loud errors (D6)', () => {
|
|
467
|
+
const reader = feedbackJs.match(
|
|
468
|
+
/function readUrlState\(\)[\s\S]*?(?=\n function )/,
|
|
469
|
+
);
|
|
470
|
+
assert.ok(reader, 'readUrlState should be defined');
|
|
471
|
+
assert.match(
|
|
472
|
+
reader[0],
|
|
473
|
+
/STATUS_VALUES\.indexOf/,
|
|
474
|
+
'readUrlState must validate ?status= against STATUS_VALUES',
|
|
475
|
+
);
|
|
476
|
+
assert.match(
|
|
477
|
+
reader[0],
|
|
478
|
+
/showUrlError\(/,
|
|
479
|
+
'readUrlState must surface invalid values via showUrlError',
|
|
480
|
+
);
|
|
481
|
+
assert.match(
|
|
482
|
+
reader[0],
|
|
483
|
+
/params\.getAll\(['"]tag['"]\)/,
|
|
484
|
+
'readUrlState must read ?tag= as a repeated-key array',
|
|
485
|
+
);
|
|
486
|
+
});
|
|
411
487
|
});
|
|
@@ -35,35 +35,80 @@
|
|
|
35
35
|
var successToast = document.getElementById('success-toast');
|
|
36
36
|
|
|
37
37
|
// ── URL handling ───────────────────────────────────────────────────────
|
|
38
|
+
//
|
|
39
|
+
// All deep-linkable view state for this page rides on
|
|
40
|
+
// `window.NexusUrl` — the shared helper auto-injected by oculus's
|
|
41
|
+
// chrome pass. The earlier inline `currentUrlParams` / `updateUrl`
|
|
42
|
+
// copies are gone (commission moix23w5).
|
|
43
|
+
//
|
|
44
|
+
// URL keys:
|
|
45
|
+
// ?status= pending | completed | rejected
|
|
46
|
+
// List-page status filter. Default 'pending'.
|
|
47
|
+
// ?feedback=ID Detail deep-link; pushes (D5).
|
|
48
|
+
// ?tag=A&tag=B Per-detail tag filter (D12). Repeated keys; clears
|
|
49
|
+
// itself when the detail closes via the omit-defaults
|
|
50
|
+
// rule.
|
|
51
|
+
|
|
52
|
+
var STATUS_VALUES = ['pending', 'completed', 'rejected'];
|
|
53
|
+
var DEFAULT_STATUS = 'pending';
|
|
54
|
+
|
|
55
|
+
function showUrlError(msg) {
|
|
56
|
+
var el = document.getElementById('url-error-banner');
|
|
57
|
+
if (!el) return;
|
|
58
|
+
var line = document.createElement('div');
|
|
59
|
+
line.textContent = msg;
|
|
60
|
+
el.appendChild(line);
|
|
61
|
+
el.style.display = 'block';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function clearUrlErrors() {
|
|
65
|
+
var el = document.getElementById('url-error-banner');
|
|
66
|
+
if (!el) return;
|
|
67
|
+
el.innerHTML = '';
|
|
68
|
+
el.style.display = 'none';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Persist the list-page status filter to the URL (replace, D5). */
|
|
72
|
+
function writeStatusFilterToUrl() {
|
|
73
|
+
var status = statusFilterEl ? statusFilterEl.value : DEFAULT_STATUS;
|
|
74
|
+
window.NexusUrl.update({
|
|
75
|
+
status: status === DEFAULT_STATUS ? null : status,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
38
78
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
79
|
+
/** Persist the per-detail tag filter to the URL (replace, D5). */
|
|
80
|
+
function writeTagFilterToUrl() {
|
|
81
|
+
var keys = Object.keys(activeTagFilters);
|
|
82
|
+
window.NexusUrl.update({
|
|
83
|
+
tag: keys.length === 0 ? null : keys,
|
|
84
|
+
});
|
|
44
85
|
}
|
|
45
86
|
|
|
46
87
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* `?feedback=ID` (D10) — keyed on the request id (req.id), which is
|
|
51
|
-
* stable across the list reorderings the 12 s polling loop can
|
|
52
|
-
* trigger. The legacy index-based showDetail path now translates
|
|
53
|
-
* index → id at the click site.
|
|
88
|
+
* Read URL state into the page. Validates the status filter and any
|
|
89
|
+
* tag values; unknowns fail loud (D6). Returns the deep-link
|
|
90
|
+
* feedback id, if any.
|
|
54
91
|
*/
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
var
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
92
|
+
function readUrlState() {
|
|
93
|
+
clearUrlErrors();
|
|
94
|
+
var params = window.NexusUrl.read();
|
|
95
|
+
|
|
96
|
+
var status = params.get('status');
|
|
97
|
+
if (status !== null) {
|
|
98
|
+
if (STATUS_VALUES.indexOf(status) !== -1) {
|
|
99
|
+
if (statusFilterEl) statusFilterEl.value = status;
|
|
100
|
+
} else {
|
|
101
|
+
showUrlError('Unknown feedback status "' + status + '". Expected one of: ' + STATUS_VALUES.join(', ') + '.');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
var tags = params.getAll('tag');
|
|
106
|
+
activeTagFilters = {};
|
|
107
|
+
for (var i = 0; i < tags.length; i++) {
|
|
108
|
+
activeTagFilters[tags[i]] = true;
|
|
63
109
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
window.history.pushState({}, '', next);
|
|
110
|
+
|
|
111
|
+
return params.get('feedback');
|
|
67
112
|
}
|
|
68
113
|
|
|
69
114
|
// ── Helpers ────────────────────────────────────────────────────────────
|
|
@@ -180,8 +225,9 @@
|
|
|
180
225
|
|
|
181
226
|
// Centralised URL push (D12) — keyed on the request id so the URL
|
|
182
227
|
// survives list reorderings between the 12 s polls. Translation
|
|
183
|
-
// index → id happens here, not at the click site.
|
|
184
|
-
|
|
228
|
+
// index → id happens here, not at the click site. Detail open is
|
|
229
|
+
// a navigation event (push: true).
|
|
230
|
+
if (!skipUrlPush) window.NexusUrl.update({ feedback: currentRequest.id }, { push: true });
|
|
185
231
|
|
|
186
232
|
// Initialize local answers from server state
|
|
187
233
|
localAnswers = {};
|
|
@@ -231,7 +277,7 @@
|
|
|
231
277
|
})
|
|
232
278
|
.then(function (req) {
|
|
233
279
|
if (!req || !req.id) {
|
|
234
|
-
if (!skipUrlPush)
|
|
280
|
+
if (!skipUrlPush) window.NexusUrl.update({ feedback: id }, { push: true });
|
|
235
281
|
renderFeedbackNotFound(id);
|
|
236
282
|
return;
|
|
237
283
|
}
|
|
@@ -242,7 +288,7 @@
|
|
|
242
288
|
showDetail(requests.length - 1, { skipUrlPush: skipUrlPush });
|
|
243
289
|
})
|
|
244
290
|
.catch(function () {
|
|
245
|
-
if (!skipUrlPush)
|
|
291
|
+
if (!skipUrlPush) window.NexusUrl.update({ feedback: id }, { push: true });
|
|
246
292
|
renderFeedbackNotFound(id);
|
|
247
293
|
});
|
|
248
294
|
}
|
|
@@ -332,6 +378,7 @@
|
|
|
332
378
|
activeTagFilters[tag] = true;
|
|
333
379
|
e.target.classList.add('active');
|
|
334
380
|
}
|
|
381
|
+
writeTagFilterToUrl();
|
|
335
382
|
applyTagFilters();
|
|
336
383
|
} else if (e.target.matches('.tag-filter-clear')) {
|
|
337
384
|
activeTagFilters = {};
|
|
@@ -339,6 +386,7 @@
|
|
|
339
386
|
for (var j = 0; j < btns.length; j++) {
|
|
340
387
|
btns[j].classList.remove('active');
|
|
341
388
|
}
|
|
389
|
+
writeTagFilterToUrl();
|
|
342
390
|
applyTagFilters();
|
|
343
391
|
}
|
|
344
392
|
});
|
|
@@ -604,8 +652,9 @@
|
|
|
604
652
|
listView.style.display = '';
|
|
605
653
|
// D11: push a clean URL so deep-link entries survive the Back
|
|
606
654
|
// button. Never pop history — the operator may have arrived
|
|
607
|
-
// directly at ?feedback=ID.
|
|
608
|
-
|
|
655
|
+
// directly at ?feedback=ID. Closing the detail also drops the
|
|
656
|
+
// per-detail tag filter via the omit-defaults rule (D12).
|
|
657
|
+
if (!skipUrlPush) window.NexusUrl.update({ feedback: null, tag: null }, { push: true });
|
|
609
658
|
fetchList();
|
|
610
659
|
startPoll();
|
|
611
660
|
}
|
|
@@ -683,6 +732,7 @@
|
|
|
683
732
|
});
|
|
684
733
|
|
|
685
734
|
statusFilterEl.addEventListener('change', function () {
|
|
735
|
+
writeStatusFilterToUrl();
|
|
686
736
|
requests = [];
|
|
687
737
|
renderList();
|
|
688
738
|
fetchList();
|
|
@@ -853,11 +903,12 @@
|
|
|
853
903
|
|
|
854
904
|
// ── Browser navigation (popstate) ──────────────────────────────────────
|
|
855
905
|
|
|
856
|
-
//
|
|
857
|
-
//
|
|
858
|
-
//
|
|
906
|
+
// Restore the full URL state on Back / Forward — ?status=,
|
|
907
|
+
// ?feedback=, and ?tag= each round-trip independently. The
|
|
908
|
+
// popstate-driven path uses skipUrlPush so it never re-pushes the
|
|
909
|
+
// URL the browser already updated.
|
|
859
910
|
window.addEventListener('popstate', function () {
|
|
860
|
-
var feedbackId =
|
|
911
|
+
var feedbackId = readUrlState();
|
|
861
912
|
if (feedbackId) {
|
|
862
913
|
showDetailById(feedbackId, { skipUrlPush: true });
|
|
863
914
|
} else {
|
|
@@ -866,17 +917,19 @@
|
|
|
866
917
|
});
|
|
867
918
|
|
|
868
919
|
// ── Init ───────────────────────────────────────────────────────────────
|
|
869
|
-
|
|
920
|
+
//
|
|
921
|
+
// Read URL state on first paint so the status filter, tag filter,
|
|
922
|
+
// and ?feedback= deep-link survive refresh and copy-paste. The list
|
|
923
|
+
// fetch already reads statusFilterEl.value, so updating that input
|
|
924
|
+
// before fetchList() is enough to apply the URL-restored status.
|
|
925
|
+
var initialFeedbackId = readUrlState();
|
|
870
926
|
fetchList();
|
|
871
927
|
startPoll();
|
|
872
928
|
|
|
873
929
|
// Deep-link: ?feedback=ID — open that request's detail after the
|
|
874
|
-
// first list fetch lands.
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
// A missing/deleted/mistyped id renders a "not found" state without
|
|
878
|
-
// rewriting the URL (D16).
|
|
879
|
-
var initialFeedbackId = currentUrlParams().get('feedback');
|
|
930
|
+
// first list fetch lands. On a miss, the /api/input/request-show
|
|
931
|
+
// fallback inside showDetailById handles it. A missing/deleted/
|
|
932
|
+
// mistyped id renders a "not found" state without rewriting the URL.
|
|
880
933
|
if (initialFeedbackId) {
|
|
881
934
|
showDetailById(initialFeedbackId, { skipUrlPush: true });
|
|
882
935
|
}
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
<main style="padding: 24px;">
|
|
11
11
|
<h1>Feedback</h1>
|
|
12
12
|
|
|
13
|
+
<!--
|
|
14
|
+
URL-state validation banner (D6 fail-loud). Shown when the page is
|
|
15
|
+
loaded with an invalid filter value (e.g. ?status=bogus). The banner
|
|
16
|
+
appends one line per invalid value; pages do not silently fall back
|
|
17
|
+
to a default.
|
|
18
|
+
-->
|
|
19
|
+
<div id="url-error-banner" class="error-msg" style="display:none;margin-bottom:12px;padding:0.6rem 0.8rem;background:var(--bg2,#1a1b26);border:1px solid var(--red,#f7768e);border-radius:6px;font-size:0.9rem;color:var(--red,#f7768e)"></div>
|
|
20
|
+
|
|
13
21
|
<!-- List view -->
|
|
14
22
|
<div id="list-view">
|
|
15
23
|
<div class="toolbar">
|
package/src/static/index.html
CHANGED
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
|
|
25
25
|
<h1>Spider</h1>
|
|
26
26
|
|
|
27
|
+
<!--
|
|
28
|
+
URL-state validation banner (D6 fail-loud). Shown when the page is
|
|
29
|
+
loaded with an invalid filter value (e.g. ?status=bogus). The banner
|
|
30
|
+
appends one line per invalid value; pages do not silently fall back
|
|
31
|
+
to a default.
|
|
32
|
+
-->
|
|
33
|
+
<div id="url-error-banner" class="error-msg" style="display:none;margin-bottom:12px;padding:0.6rem 0.8rem;background:var(--bg2,#1a1b26);border:1px solid var(--red,#f7768e);border-radius:6px;font-size:0.9rem;color:var(--red,#f7768e)"></div>
|
|
34
|
+
|
|
27
35
|
<div class="tab-bar">
|
|
28
36
|
<button class="tab active" data-tab="rigs">Rigs</button>
|
|
29
37
|
<button class="tab" data-tab="config">Config</button>
|
|
@@ -1917,33 +1917,37 @@ describe('spider.js engine-detail attempt-history details', () => {
|
|
|
1917
1917
|
// ── Deep-link URL state (?rig=ID) ──────────────────────────────────────
|
|
1918
1918
|
|
|
1919
1919
|
describe('spider.js — deep-link URL state', () => {
|
|
1920
|
-
it('
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
'currentUrlParams
|
|
1920
|
+
it('routes URL reads/writes through window.NexusUrl (no inline helpers)', () => {
|
|
1921
|
+
// Inline currentUrlParams / updateUrl are gone (commission moix23w5).
|
|
1922
|
+
assert.ok(
|
|
1923
|
+
!/function\s+currentUrlParams\s*\(/.test(spiderJs),
|
|
1924
|
+
'inline currentUrlParams must not be redeclared',
|
|
1925
|
+
);
|
|
1926
|
+
assert.ok(
|
|
1927
|
+
!/function\s+updateUrl\s*\(/.test(spiderJs),
|
|
1928
|
+
'inline updateUrl must not be redeclared',
|
|
1925
1929
|
);
|
|
1926
1930
|
assert.match(
|
|
1927
1931
|
spiderJs,
|
|
1928
|
-
/
|
|
1929
|
-
'
|
|
1932
|
+
/window\.NexusUrl\.read\(\)/,
|
|
1933
|
+
'spider.js should read URL state via window.NexusUrl.read',
|
|
1930
1934
|
);
|
|
1931
1935
|
assert.match(
|
|
1932
1936
|
spiderJs,
|
|
1933
|
-
/
|
|
1934
|
-
'
|
|
1937
|
+
/window\.NexusUrl\.update\(/,
|
|
1938
|
+
'spider.js should write URL state via window.NexusUrl.update',
|
|
1935
1939
|
);
|
|
1936
1940
|
});
|
|
1937
1941
|
|
|
1938
|
-
it('showRigDetail pushes ?rig=ID via
|
|
1942
|
+
it('showRigDetail pushes ?rig=ID via NexusUrl with push: true (D12)', () => {
|
|
1939
1943
|
const block = spiderJs.match(
|
|
1940
1944
|
/function showRigDetail\(rig(?:, opts)?\)[\s\S]*?(?=\n function )/,
|
|
1941
1945
|
);
|
|
1942
1946
|
assert.ok(block, 'should find showRigDetail body');
|
|
1943
1947
|
assert.match(
|
|
1944
1948
|
block[0],
|
|
1945
|
-
/
|
|
1946
|
-
'showRigDetail
|
|
1949
|
+
/window\.NexusUrl\.update\(\{\s*rig:\s*rig\.id\s*\}\s*,\s*\{\s*push:\s*true\s*\}\)/,
|
|
1950
|
+
'showRigDetail must push ?rig=<rig.id> when not skipUrlPush',
|
|
1947
1951
|
);
|
|
1948
1952
|
assert.match(
|
|
1949
1953
|
block[0],
|
|
@@ -1952,15 +1956,15 @@ describe('spider.js — deep-link URL state', () => {
|
|
|
1952
1956
|
);
|
|
1953
1957
|
});
|
|
1954
1958
|
|
|
1955
|
-
it('backToList clears ?rig via
|
|
1959
|
+
it('backToList clears ?rig via NexusUrl with push: true — never history.back (D11)', () => {
|
|
1956
1960
|
const block = spiderJs.match(
|
|
1957
1961
|
/function backToList\((?:opts)?\)[\s\S]*?(?=\n function )/,
|
|
1958
1962
|
);
|
|
1959
1963
|
assert.ok(block, 'should find backToList body');
|
|
1960
1964
|
assert.match(
|
|
1961
1965
|
block[0],
|
|
1962
|
-
/
|
|
1963
|
-
'backToList
|
|
1966
|
+
/window\.NexusUrl\.update\(\{\s*rig:\s*null\s*\}\s*,\s*\{\s*push:\s*true\s*\}\)/,
|
|
1967
|
+
'backToList must push a clean URL via NexusUrl.update({rig: null}, {push: true})',
|
|
1964
1968
|
);
|
|
1965
1969
|
assert.ok(
|
|
1966
1970
|
!/history\.back\(/.test(block[0]),
|
|
@@ -1980,8 +1984,8 @@ describe('spider.js — deep-link URL state', () => {
|
|
|
1980
1984
|
assert.ok(block, 'should find popstate handler body');
|
|
1981
1985
|
assert.match(
|
|
1982
1986
|
block[0],
|
|
1983
|
-
/
|
|
1984
|
-
'popstate handler
|
|
1987
|
+
/readUrlState\(\)/,
|
|
1988
|
+
'popstate handler should restore filter state via readUrlState',
|
|
1985
1989
|
);
|
|
1986
1990
|
assert.match(
|
|
1987
1991
|
block[0],
|
|
@@ -1990,15 +1994,15 @@ describe('spider.js — deep-link URL state', () => {
|
|
|
1990
1994
|
);
|
|
1991
1995
|
});
|
|
1992
1996
|
|
|
1993
|
-
it('init reads
|
|
1997
|
+
it('init reads URL state and opens the deep-linked rig after the list lands', () => {
|
|
1994
1998
|
assert.match(
|
|
1995
1999
|
spiderJs,
|
|
1996
|
-
/
|
|
1997
|
-
'init
|
|
2000
|
+
/var\s+initialState\s*=\s*readUrlState\(\)/,
|
|
2001
|
+
'init should call readUrlState before fetching rigs',
|
|
1998
2002
|
);
|
|
1999
2003
|
assert.match(
|
|
2000
2004
|
spiderJs,
|
|
2001
|
-
/showRigDetailById\(\s*
|
|
2005
|
+
/showRigDetailById\(\s*initialState\.rigId\s*,\s*\{\s*skipUrlPush:\s*true\s*\}\s*\)/,
|
|
2002
2006
|
'init opens the detail via showRigDetailById with skipUrlPush=true',
|
|
2003
2007
|
);
|
|
2004
2008
|
});
|
|
@@ -2008,10 +2012,10 @@ describe('spider.js — deep-link URL state', () => {
|
|
|
2008
2012
|
/function renderRigDetailNotFound\([\s\S]*?(?=\n function )/,
|
|
2009
2013
|
);
|
|
2010
2014
|
assert.ok(block, 'should find renderRigDetailNotFound body');
|
|
2011
|
-
// The function itself must not call
|
|
2012
|
-
// alone so the operator can recover by editing the address bar.
|
|
2015
|
+
// The function itself must not call NexusUrl.update — the URL is
|
|
2016
|
+
// left alone so the operator can recover by editing the address bar.
|
|
2013
2017
|
assert.ok(
|
|
2014
|
-
!/
|
|
2018
|
+
!/NexusUrl\.update/.test(block[0]),
|
|
2015
2019
|
'renderRigDetailNotFound must not rewrite the URL',
|
|
2016
2020
|
);
|
|
2017
2021
|
assert.match(
|
|
@@ -2022,14 +2026,76 @@ describe('spider.js — deep-link URL state', () => {
|
|
|
2022
2026
|
});
|
|
2023
2027
|
|
|
2024
2028
|
it('does not push a tab=… or engine=… URL param (D13/D14 explicit out-of-scope)', () => {
|
|
2029
|
+
// Filter-shaped only (D13 narrow reading): the rigs page must
|
|
2030
|
+
// never write ?tab= or ?engine= to the URL. These assertions stay
|
|
2031
|
+
// in place verbatim from the pre-migration test suite.
|
|
2025
2032
|
assert.ok(
|
|
2026
|
-
!/
|
|
2033
|
+
!/NexusUrl\.update\(\{\s*tab:/.test(spiderJs),
|
|
2027
2034
|
'tab state must remain client-only (D14)',
|
|
2028
2035
|
);
|
|
2029
2036
|
assert.ok(
|
|
2030
|
-
!/
|
|
2037
|
+
!/NexusUrl\.update\(\{\s*engine:/.test(spiderJs),
|
|
2031
2038
|
'engine selection must remain client state (D13)',
|
|
2032
2039
|
);
|
|
2040
|
+
// Defensive — also forbid the legacy updateUrl shape (which the
|
|
2041
|
+
// page no longer carries, but which a future regression could
|
|
2042
|
+
// reintroduce).
|
|
2043
|
+
assert.ok(
|
|
2044
|
+
!/updateUrl\(\{\s*tab:/.test(spiderJs),
|
|
2045
|
+
'tab state must remain client-only (D14) — legacy form too',
|
|
2046
|
+
);
|
|
2047
|
+
assert.ok(
|
|
2048
|
+
!/updateUrl\(\{\s*engine:/.test(spiderJs),
|
|
2049
|
+
'engine selection must remain client state (D13) — legacy form too',
|
|
2050
|
+
);
|
|
2051
|
+
});
|
|
2052
|
+
|
|
2053
|
+
it('rigs filter state writes through NexusUrl.update (replace, no push: true)', () => {
|
|
2054
|
+
const writer = spiderJs.match(
|
|
2055
|
+
/function writeRigFiltersToUrl\(\)[\s\S]*?(?=\n function )/,
|
|
2056
|
+
);
|
|
2057
|
+
assert.ok(writer, 'writeRigFiltersToUrl should be defined');
|
|
2058
|
+
assert.match(
|
|
2059
|
+
writer[0],
|
|
2060
|
+
/window\.NexusUrl\.update\(\{[\s\S]*?status:[\s\S]*?\}\s*\)/,
|
|
2061
|
+
'rig filter writer must call NexusUrl.update without { push: true }',
|
|
2062
|
+
);
|
|
2063
|
+
assert.ok(
|
|
2064
|
+
!/push:\s*true/.test(writer[0]),
|
|
2065
|
+
'rig filter writes must use replaceState (D5 default) — no push: true',
|
|
2066
|
+
);
|
|
2067
|
+
// Each filter key the rigs page tracks must appear in the writer.
|
|
2068
|
+
for (const key of ['status', "'writ-filter'", 'from', 'to', 'sort', 'dir']) {
|
|
2069
|
+
assert.match(
|
|
2070
|
+
writer[0],
|
|
2071
|
+
new RegExp(`${key.replace(/'/g, "\\'")}\\s*:`),
|
|
2072
|
+
`rig filter writer must include the ${key} key`,
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
|
|
2077
|
+
it('readUrlState validates filter values and surfaces fail-loud errors (D6)', () => {
|
|
2078
|
+
const reader = spiderJs.match(
|
|
2079
|
+
/function readUrlState\(\)[\s\S]*?(?=\n function )/,
|
|
2080
|
+
);
|
|
2081
|
+
assert.ok(reader, 'readUrlState should be defined');
|
|
2082
|
+
// The reader must call showUrlError when validation fails.
|
|
2083
|
+
assert.match(
|
|
2084
|
+
reader[0],
|
|
2085
|
+
/showUrlError\(/,
|
|
2086
|
+
'readUrlState must surface validation errors via showUrlError',
|
|
2087
|
+
);
|
|
2088
|
+
// It must validate ?status= against the known status set.
|
|
2089
|
+
assert.match(
|
|
2090
|
+
reader[0],
|
|
2091
|
+
/STATUS_VALUES\.indexOf/,
|
|
2092
|
+
'readUrlState must validate ?status= against STATUS_VALUES',
|
|
2093
|
+
);
|
|
2094
|
+
assert.match(
|
|
2095
|
+
reader[0],
|
|
2096
|
+
/SORT_FIELDS\.indexOf/,
|
|
2097
|
+
'readUrlState must validate ?sort= against SORT_FIELDS',
|
|
2098
|
+
);
|
|
2033
2099
|
});
|
|
2034
2100
|
});
|
|
2035
2101
|
|
package/src/static/spider.js
CHANGED
|
@@ -77,33 +77,115 @@
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// ── URL handling ───────────────────────────────────────────────────────
|
|
80
|
+
//
|
|
81
|
+
// All deep-linkable view state for this page rides on
|
|
82
|
+
// `window.NexusUrl` — the shared helper auto-injected by oculus's
|
|
83
|
+
// chrome pass. The earlier inline `currentUrlParams` / `updateUrl`
|
|
84
|
+
// copies are gone (commission moix23w5).
|
|
85
|
+
//
|
|
86
|
+
// URL keys for the rigs tab:
|
|
87
|
+
// ?status= running | completed | failed | cancelled
|
|
88
|
+
// (matches the server-side ?status= already sent
|
|
89
|
+
// to /api/rig/list per D8). Default '' (All).
|
|
90
|
+
// ?writ-filter= Substring filter against rig.writTitle / writId.
|
|
91
|
+
// Default ''.
|
|
92
|
+
// ?from= ISO date (yyyy-mm-dd). Inclusive lower bound.
|
|
93
|
+
// ?to= ISO date. Inclusive upper bound (compared with
|
|
94
|
+
// T23:59:59 suffix).
|
|
95
|
+
// ?sort= status | id | writId | createdAt
|
|
96
|
+
// Default 'createdAt'; omitted when default.
|
|
97
|
+
// ?dir= asc | desc. Default 'desc'.
|
|
98
|
+
// ?rig=ID Detail deep-link; pushes (D5).
|
|
99
|
+
//
|
|
100
|
+
// Tab state (?tab=) and engine selection (?engine=) are deliberately
|
|
101
|
+
// NOT URL-tracked (D13 — narrow reading). The spider-ui tests assert
|
|
102
|
+
// those keys never appear; do not introduce them.
|
|
103
|
+
|
|
104
|
+
var SORT_FIELDS = ['status', 'id', 'writId', 'createdAt'];
|
|
105
|
+
var SORT_DIRS = ['asc', 'desc'];
|
|
106
|
+
var STATUS_VALUES = ['', 'running', 'completed', 'failed', 'cancelled'];
|
|
107
|
+
|
|
108
|
+
/** Append a fail-loud message to the URL-error banner (D6). */
|
|
109
|
+
function showUrlError(msg) {
|
|
110
|
+
var el = document.getElementById('url-error-banner');
|
|
111
|
+
if (!el) return;
|
|
112
|
+
var line = document.createElement('div');
|
|
113
|
+
line.textContent = msg;
|
|
114
|
+
el.appendChild(line);
|
|
115
|
+
el.style.display = 'block';
|
|
116
|
+
}
|
|
80
117
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
118
|
+
function clearUrlErrors() {
|
|
119
|
+
var el = document.getElementById('url-error-banner');
|
|
120
|
+
if (!el) return;
|
|
121
|
+
el.innerHTML = '';
|
|
122
|
+
el.style.display = 'none';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Persist the current rigs-list filter state to the URL (replace, D5). */
|
|
126
|
+
function writeRigFiltersToUrl() {
|
|
127
|
+
var writFilter = (document.getElementById('writ-filter') || {}).value || '';
|
|
128
|
+
var dateFrom = (document.getElementById('date-from') || {}).value || '';
|
|
129
|
+
var dateTo = (document.getElementById('date-to') || {}).value || '';
|
|
130
|
+
window.NexusUrl.update({
|
|
131
|
+
status: currentStatusFilter === '' ? null : currentStatusFilter,
|
|
132
|
+
'writ-filter': writFilter === '' ? null : writFilter,
|
|
133
|
+
from: dateFrom === '' ? null : dateFrom,
|
|
134
|
+
to: dateTo === '' ? null : dateTo,
|
|
135
|
+
sort: sortField === 'createdAt' ? null : sortField,
|
|
136
|
+
dir: sortDir === 'desc' ? null : sortDir,
|
|
137
|
+
});
|
|
87
138
|
}
|
|
88
139
|
|
|
89
140
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
141
|
+
* Read the URL filter state into module-level variables and return
|
|
142
|
+
* the deep-link rig id (if any). Validates each value against its
|
|
143
|
+
* known set; unknowns surface a fail-loud banner per D6 without
|
|
144
|
+
* applying the value.
|
|
94
145
|
*/
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
var
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
146
|
+
function readUrlState() {
|
|
147
|
+
clearUrlErrors();
|
|
148
|
+
var params = window.NexusUrl.read();
|
|
149
|
+
|
|
150
|
+
var status = params.get('status');
|
|
151
|
+
if (status !== null) {
|
|
152
|
+
if (STATUS_VALUES.indexOf(status) !== -1) {
|
|
153
|
+
currentStatusFilter = status;
|
|
154
|
+
} else {
|
|
155
|
+
showUrlError('Unknown rig status "' + status + '". Expected one of: running, completed, failed, cancelled.');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
var sort = params.get('sort');
|
|
160
|
+
if (sort !== null) {
|
|
161
|
+
if (SORT_FIELDS.indexOf(sort) !== -1) sortField = sort;
|
|
162
|
+
else showUrlError('Unknown sort column "' + sort + '". Expected one of: ' + SORT_FIELDS.join(', ') + '.');
|
|
163
|
+
}
|
|
164
|
+
var dir = params.get('dir');
|
|
165
|
+
if (dir !== null) {
|
|
166
|
+
if (SORT_DIRS.indexOf(dir) !== -1) sortDir = dir;
|
|
167
|
+
else showUrlError('Unknown sort direction "' + dir + '". Expected "asc" or "desc".');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
rigId: params.get('rig'),
|
|
172
|
+
writFilter: params.get('writ-filter'),
|
|
173
|
+
dateFrom: params.get('from'),
|
|
174
|
+
dateTo: params.get('to'),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Sync the toolbar UI to current filter state. */
|
|
179
|
+
function syncRigFilterUiFromState() {
|
|
180
|
+
var statusEl = document.getElementById('status-filter');
|
|
181
|
+
if (statusEl) statusEl.value = currentStatusFilter;
|
|
182
|
+
var params = window.NexusUrl.read();
|
|
183
|
+
var writEl = document.getElementById('writ-filter');
|
|
184
|
+
if (writEl) writEl.value = params.get('writ-filter') || '';
|
|
185
|
+
var fromEl = document.getElementById('date-from');
|
|
186
|
+
if (fromEl) fromEl.value = params.get('from') || '';
|
|
187
|
+
var toEl = document.getElementById('date-to');
|
|
188
|
+
if (toEl) toEl.value = params.get('to') || '';
|
|
107
189
|
}
|
|
108
190
|
|
|
109
191
|
// ── Utility ────────────────────────────────────────────────────────────
|
|
@@ -918,12 +1000,12 @@
|
|
|
918
1000
|
if (rig && rig.id) {
|
|
919
1001
|
showRigDetail(rig, { skipUrlPush: skipUrlPush });
|
|
920
1002
|
} else {
|
|
921
|
-
if (!skipUrlPush)
|
|
1003
|
+
if (!skipUrlPush) window.NexusUrl.update({ rig: id }, { push: true });
|
|
922
1004
|
renderRigDetailNotFound(id);
|
|
923
1005
|
}
|
|
924
1006
|
})
|
|
925
1007
|
.catch(function () {
|
|
926
|
-
if (!skipUrlPush)
|
|
1008
|
+
if (!skipUrlPush) window.NexusUrl.update({ rig: id }, { push: true });
|
|
927
1009
|
renderRigDetailNotFound(id);
|
|
928
1010
|
});
|
|
929
1011
|
}
|
|
@@ -937,8 +1019,8 @@
|
|
|
937
1019
|
// (writ-title anchor, rig-id anchor, future entry points, deep-link
|
|
938
1020
|
// init) emits ?rig=ID for free. The popstate-driven path passes
|
|
939
1021
|
// skipUrlPush=true to avoid double-pushing the URL the browser
|
|
940
|
-
// already updated.
|
|
941
|
-
if (!skipUrlPush)
|
|
1022
|
+
// already updated. Detail open is a navigation event (push: true).
|
|
1023
|
+
if (!skipUrlPush) window.NexusUrl.update({ rig: rig.id }, { push: true });
|
|
942
1024
|
|
|
943
1025
|
// Reset the session-log surface BEFORE any render (T7): hides the
|
|
944
1026
|
// section, clears the textarea, and nulls transcript state.
|
|
@@ -1618,7 +1700,7 @@
|
|
|
1618
1700
|
// doing what they expect. We deliberately push instead of popping
|
|
1619
1701
|
// history — the operator may have arrived directly at ?rig=ID with
|
|
1620
1702
|
// no prior list-view entry to pop back to.
|
|
1621
|
-
if (!skipUrlPush)
|
|
1703
|
+
if (!skipUrlPush) window.NexusUrl.update({ rig: null }, { push: true });
|
|
1622
1704
|
}
|
|
1623
1705
|
|
|
1624
1706
|
// ── Config tab ─────────────────────────────────────────────────────────
|
|
@@ -1893,6 +1975,8 @@
|
|
|
1893
1975
|
var statusFilter = document.getElementById('status-filter');
|
|
1894
1976
|
if (statusFilter) {
|
|
1895
1977
|
statusFilter.addEventListener('change', function () {
|
|
1978
|
+
currentStatusFilter = statusFilter.value;
|
|
1979
|
+
writeRigFiltersToUrl();
|
|
1896
1980
|
fetchRigs(statusFilter.value);
|
|
1897
1981
|
});
|
|
1898
1982
|
}
|
|
@@ -1901,6 +1985,7 @@
|
|
|
1901
1985
|
var writFilter = document.getElementById('writ-filter');
|
|
1902
1986
|
if (writFilter) {
|
|
1903
1987
|
writFilter.addEventListener('input', function () {
|
|
1988
|
+
writeRigFiltersToUrl();
|
|
1904
1989
|
renderRigList();
|
|
1905
1990
|
});
|
|
1906
1991
|
}
|
|
@@ -1909,10 +1994,16 @@
|
|
|
1909
1994
|
var dateFrom = document.getElementById('date-from');
|
|
1910
1995
|
var dateTo = document.getElementById('date-to');
|
|
1911
1996
|
if (dateFrom) {
|
|
1912
|
-
dateFrom.addEventListener('change', function () {
|
|
1997
|
+
dateFrom.addEventListener('change', function () {
|
|
1998
|
+
writeRigFiltersToUrl();
|
|
1999
|
+
renderRigList();
|
|
2000
|
+
});
|
|
1913
2001
|
}
|
|
1914
2002
|
if (dateTo) {
|
|
1915
|
-
dateTo.addEventListener('change', function () {
|
|
2003
|
+
dateTo.addEventListener('change', function () {
|
|
2004
|
+
writeRigFiltersToUrl();
|
|
2005
|
+
renderRigList();
|
|
2006
|
+
});
|
|
1916
2007
|
}
|
|
1917
2008
|
|
|
1918
2009
|
// Refresh button
|
|
@@ -1937,6 +2028,7 @@
|
|
|
1937
2028
|
sortField = field;
|
|
1938
2029
|
sortDir = 'asc';
|
|
1939
2030
|
}
|
|
2031
|
+
writeRigFiltersToUrl();
|
|
1940
2032
|
renderRigList();
|
|
1941
2033
|
});
|
|
1942
2034
|
})(headers[k]);
|
|
@@ -1948,29 +2040,33 @@
|
|
|
1948
2040
|
backBtn.addEventListener('click', backToList);
|
|
1949
2041
|
}
|
|
1950
2042
|
|
|
1951
|
-
// Browser navigation (Back / Forward) —
|
|
1952
|
-
//
|
|
1953
|
-
//
|
|
1954
|
-
//
|
|
2043
|
+
// Browser navigation (Back / Forward) — restore the FULL filter
|
|
2044
|
+
// state and the ?rig= deep-link. Pairs with the central push
|
|
2045
|
+
// inside showRigDetail (D11/D12). The popstate-driven path uses
|
|
2046
|
+
// skipUrlPush so it never re-pushes the URL the browser already
|
|
2047
|
+
// updated.
|
|
1955
2048
|
window.addEventListener('popstate', function () {
|
|
1956
|
-
var
|
|
1957
|
-
|
|
1958
|
-
|
|
2049
|
+
var state = readUrlState();
|
|
2050
|
+
syncRigFilterUiFromState();
|
|
2051
|
+
if (state.rigId) {
|
|
2052
|
+
showRigDetailById(state.rigId, { skipUrlPush: true });
|
|
1959
2053
|
} else {
|
|
1960
2054
|
backToList({ skipUrlPush: true });
|
|
2055
|
+
// Refresh the list with the restored filter set.
|
|
2056
|
+
fetchRigs(currentStatusFilter);
|
|
1961
2057
|
}
|
|
1962
2058
|
});
|
|
1963
2059
|
|
|
1964
|
-
// Initial load.
|
|
1965
|
-
//
|
|
1966
|
-
//
|
|
1967
|
-
//
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
fetchRigs(
|
|
2060
|
+
// Initial load. Read the URL filter state and ?rig= deep-link
|
|
2061
|
+
// BEFORE fetching the rig list so the server query already carries
|
|
2062
|
+
// the correct ?status= filter. A missing/deleted ?rig= id falls
|
|
2063
|
+
// through to renderRigDetailNotFound (D16) — the URL is preserved.
|
|
2064
|
+
var initialState = readUrlState();
|
|
2065
|
+
syncRigFilterUiFromState();
|
|
2066
|
+
fetchRigs(currentStatusFilter, {
|
|
1971
2067
|
onLoaded: function () {
|
|
1972
|
-
if (
|
|
1973
|
-
showRigDetailById(
|
|
2068
|
+
if (initialState.rigId) {
|
|
2069
|
+
showRigDetailById(initialState.rigId, { skipUrlPush: true });
|
|
1974
2070
|
}
|
|
1975
2071
|
},
|
|
1976
2072
|
});
|