@shardworks/astrolabe-apparatus 0.1.280 → 0.1.282
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 +10 -10
- package/pages/astrolabe/astrolabe.js +76 -27
- package/pages/astrolabe/astrolabe.test.js +68 -11
- package/pages/astrolabe/index.html +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardworks/astrolabe-apparatus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.282",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,18 +20,18 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"zod": "4.3.6",
|
|
23
|
-
"@shardworks/
|
|
24
|
-
"@shardworks/
|
|
25
|
-
"@shardworks/
|
|
26
|
-
"@shardworks/
|
|
27
|
-
"@shardworks/
|
|
28
|
-
"@shardworks/
|
|
29
|
-
"@shardworks/
|
|
30
|
-
"@shardworks/
|
|
23
|
+
"@shardworks/stacks-apparatus": "0.1.282",
|
|
24
|
+
"@shardworks/tools-apparatus": "0.1.282",
|
|
25
|
+
"@shardworks/clerk-apparatus": "0.1.282",
|
|
26
|
+
"@shardworks/fabricator-apparatus": "0.1.282",
|
|
27
|
+
"@shardworks/loom-apparatus": "0.1.282",
|
|
28
|
+
"@shardworks/animator-apparatus": "0.1.282",
|
|
29
|
+
"@shardworks/clockworks-apparatus": "0.1.282",
|
|
30
|
+
"@shardworks/spider-apparatus": "0.1.282"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "25.5.0",
|
|
34
|
-
"@shardworks/nexus-core": "0.1.
|
|
34
|
+
"@shardworks/nexus-core": "0.1.282"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"dist",
|
|
@@ -16,34 +16,78 @@
|
|
|
16
16
|
var writTitleLookup = {};
|
|
17
17
|
|
|
18
18
|
// ── URL handling ───────────────────────────────────────────────────────
|
|
19
|
+
//
|
|
20
|
+
// All deep-linkable view state for this page rides on
|
|
21
|
+
// `window.NexusUrl` — the shared helper auto-injected by oculus's
|
|
22
|
+
// chrome pass. The earlier inline `currentUrlParams` / `updateUrl`
|
|
23
|
+
// copies are gone (commission moix23w5).
|
|
24
|
+
//
|
|
25
|
+
// URL keys:
|
|
26
|
+
// ?status= reading | analyzing | reviewing | writing |
|
|
27
|
+
// completed | failed
|
|
28
|
+
// Matches the server-side ?status= sent to
|
|
29
|
+
// /api/plan/list (D8). Default '' (All).
|
|
30
|
+
// ?plan=ID Detail deep-link; pushes (D5).
|
|
31
|
+
//
|
|
32
|
+
// Detail-tab state (inventory / scope / decisions / observations /
|
|
33
|
+
// spec) is deliberately NOT URL-tracked (D13 narrow reading). The
|
|
34
|
+
// astrolabe.test.js suite asserts no `?tab=` key is ever written;
|
|
35
|
+
// do not introduce one.
|
|
36
|
+
|
|
37
|
+
var STATUS_VALUES = ['', 'reading', 'analyzing', 'reviewing', 'writing', 'completed', 'failed'];
|
|
38
|
+
|
|
39
|
+
function showUrlError(msg) {
|
|
40
|
+
var el = document.getElementById('url-error-banner');
|
|
41
|
+
if (!el) return;
|
|
42
|
+
var line = document.createElement('div');
|
|
43
|
+
line.textContent = msg;
|
|
44
|
+
el.appendChild(line);
|
|
45
|
+
el.style.display = 'block';
|
|
46
|
+
}
|
|
19
47
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
48
|
+
function clearUrlErrors() {
|
|
49
|
+
var el = document.getElementById('url-error-banner');
|
|
50
|
+
if (!el) return;
|
|
51
|
+
el.innerHTML = '';
|
|
52
|
+
el.style.display = 'none';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Persist the list-page status filter to the URL (replace, D5). */
|
|
56
|
+
function writeStatusFilterToUrl() {
|
|
57
|
+
window.NexusUrl.update({
|
|
58
|
+
status: currentStatusFilter === '' ? null : currentStatusFilter,
|
|
59
|
+
});
|
|
26
60
|
}
|
|
27
61
|
|
|
28
62
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* is preserved verbatim — operators have URLs in the wild already
|
|
33
|
-
* (D10).
|
|
63
|
+
* Read URL state into module-level variables. Validates ?status=
|
|
64
|
+
* against STATUS_VALUES; unknowns surface a fail-loud banner per D6
|
|
65
|
+
* without applying. Returns the deep-link plan id, if any.
|
|
34
66
|
*/
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
var
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
67
|
+
function readUrlState() {
|
|
68
|
+
clearUrlErrors();
|
|
69
|
+
var params = window.NexusUrl.read();
|
|
70
|
+
|
|
71
|
+
var status = params.get('status');
|
|
72
|
+
if (status !== null) {
|
|
73
|
+
if (STATUS_VALUES.indexOf(status) !== -1) {
|
|
74
|
+
currentStatusFilter = status;
|
|
75
|
+
} else {
|
|
76
|
+
showUrlError('Unknown plan status "' + status + '". Expected one of: reading, analyzing, reviewing, writing, completed, failed.');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return params.get('plan');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Sync the status-filter button row to the current filter state. */
|
|
84
|
+
function syncStatusFilterUiFromState() {
|
|
85
|
+
var btns = document.querySelectorAll('#status-filters .filter-btn');
|
|
86
|
+
for (var i = 0; i < btns.length; i++) {
|
|
87
|
+
var match = btns[i].getAttribute('data-status') === currentStatusFilter;
|
|
88
|
+
if (match) btns[i].classList.add('active-filter');
|
|
89
|
+
else btns[i].classList.remove('active-filter');
|
|
43
90
|
}
|
|
44
|
-
var qs = params.toString();
|
|
45
|
-
var next = window.location.pathname + (qs ? '?' + qs : '');
|
|
46
|
-
window.history.pushState({}, '', next);
|
|
47
91
|
}
|
|
48
92
|
|
|
49
93
|
// ── Utility ────────────────────────────────────────────────────────────
|
|
@@ -205,6 +249,7 @@
|
|
|
205
249
|
filterBtns[j].classList.remove('active-filter');
|
|
206
250
|
}
|
|
207
251
|
btn.classList.add('active-filter');
|
|
252
|
+
writeStatusFilterToUrl();
|
|
208
253
|
fetchPlans(true);
|
|
209
254
|
});
|
|
210
255
|
})(filterBtns[fi]);
|
|
@@ -363,7 +408,7 @@
|
|
|
363
408
|
// ?plan=ID for free. The popstate-driven path passes
|
|
364
409
|
// skipUrlPush=true to avoid double-pushing the URL the browser
|
|
365
410
|
// already updated.
|
|
366
|
-
if (!skipUrlPush)
|
|
411
|
+
if (!skipUrlPush) window.NexusUrl.update({ plan: plan.id }, { push: true });
|
|
367
412
|
|
|
368
413
|
detailTitle.textContent = 'Plan: ' + plan.id;
|
|
369
414
|
|
|
@@ -674,7 +719,7 @@
|
|
|
674
719
|
// D11: push a clean URL — the operator's Forward button still does
|
|
675
720
|
// what they expect, and we never pop history because they may have
|
|
676
721
|
// arrived directly at ?plan=ID with no prior list-view entry.
|
|
677
|
-
if (!skipUrlPush)
|
|
722
|
+
if (!skipUrlPush) window.NexusUrl.update({ plan: null }, { push: true });
|
|
678
723
|
}
|
|
679
724
|
|
|
680
725
|
// ── Deep Link ──────────────────────────────────────────────────────────
|
|
@@ -693,7 +738,10 @@
|
|
|
693
738
|
*/
|
|
694
739
|
function handleDeepLink(opts) {
|
|
695
740
|
var fetchOnEmpty = !(opts && opts.fetchOnEmpty === false);
|
|
696
|
-
|
|
741
|
+
// Restore the status filter alongside the deep-link id so the
|
|
742
|
+
// filtered view round-trips through refresh and Back/Forward.
|
|
743
|
+
var planId = readUrlState();
|
|
744
|
+
syncStatusFilterUiFromState();
|
|
697
745
|
|
|
698
746
|
if (planId) {
|
|
699
747
|
fetch('/api/plan/show?planId=' + encodeURIComponent(planId))
|
|
@@ -713,9 +761,10 @@
|
|
|
713
761
|
} else if (fetchOnEmpty) {
|
|
714
762
|
fetchPlans(true);
|
|
715
763
|
} else {
|
|
716
|
-
// popstate to a no-?plan URL:
|
|
717
|
-
//
|
|
764
|
+
// popstate to a no-?plan URL: refresh the list with the
|
|
765
|
+
// restored filter and switch back to list view.
|
|
718
766
|
backToList({ skipUrlPush: true });
|
|
767
|
+
fetchPlans(true);
|
|
719
768
|
}
|
|
720
769
|
}
|
|
721
770
|
|
|
@@ -712,41 +712,50 @@ describe('astrolabe.js cost-panel rig lookup uses rig-for-writ', () => {
|
|
|
712
712
|
// ── Deep-link URL state (?plan=ID) ──────────────────────────────────────
|
|
713
713
|
|
|
714
714
|
describe('astrolabe.js — deep-link URL state', () => {
|
|
715
|
-
it('
|
|
715
|
+
it('routes URL reads/writes through window.NexusUrl (no inline helpers)', () => {
|
|
716
|
+
// Inline currentUrlParams / updateUrl are gone (commission moix23w5).
|
|
717
|
+
assert.ok(
|
|
718
|
+
!/function\s+currentUrlParams\s*\(/.test(astrolabeJs),
|
|
719
|
+
'inline currentUrlParams must not be redeclared',
|
|
720
|
+
);
|
|
721
|
+
assert.ok(
|
|
722
|
+
!/function\s+updateUrl\s*\(/.test(astrolabeJs),
|
|
723
|
+
'inline updateUrl must not be redeclared',
|
|
724
|
+
);
|
|
716
725
|
assert.match(
|
|
717
726
|
astrolabeJs,
|
|
718
|
-
/
|
|
719
|
-
'
|
|
727
|
+
/window\.NexusUrl\.read\(\)/,
|
|
728
|
+
'astrolabe.js should read URL state via window.NexusUrl.read',
|
|
720
729
|
);
|
|
721
730
|
assert.match(
|
|
722
731
|
astrolabeJs,
|
|
723
|
-
/
|
|
724
|
-
'
|
|
732
|
+
/window\.NexusUrl\.update\(/,
|
|
733
|
+
'astrolabe.js should write URL state via window.NexusUrl.update',
|
|
725
734
|
);
|
|
726
735
|
});
|
|
727
736
|
|
|
728
|
-
it('showPlanDetail pushes ?plan=ID via
|
|
737
|
+
it('showPlanDetail pushes ?plan=ID via NexusUrl with push: true (D12)', () => {
|
|
729
738
|
const block = astrolabeJs.match(
|
|
730
739
|
/function showPlanDetail\(plan(?:, opts)?\)[\s\S]*?(?=\n {2}function )/,
|
|
731
740
|
);
|
|
732
741
|
assert.ok(block, 'should find showPlanDetail body');
|
|
733
742
|
assert.match(
|
|
734
743
|
block[0],
|
|
735
|
-
/
|
|
744
|
+
/window\.NexusUrl\.update\(\{\s*plan:\s*plan\.id\s*\}\s*,\s*\{\s*push:\s*true\s*\}\)/,
|
|
736
745
|
'showPlanDetail should push ?plan=<plan.id> when not skipUrlPush',
|
|
737
746
|
);
|
|
738
747
|
assert.match(block[0], /skipUrlPush/, 'showPlanDetail accepts a skipUrlPush opt');
|
|
739
748
|
});
|
|
740
749
|
|
|
741
|
-
it('backToList clears ?plan via
|
|
750
|
+
it('backToList clears ?plan via NexusUrl with push: true — never pops history (D11)', () => {
|
|
742
751
|
const block = astrolabeJs.match(
|
|
743
752
|
/function backToList\((?:opts)?\)[\s\S]*?(?=\n {2}\/\/)/,
|
|
744
753
|
);
|
|
745
754
|
assert.ok(block, 'should find backToList body');
|
|
746
755
|
assert.match(
|
|
747
756
|
block[0],
|
|
748
|
-
/
|
|
749
|
-
'backToList should push a clean URL via
|
|
757
|
+
/window\.NexusUrl\.update\(\{\s*plan:\s*null\s*\}\s*,\s*\{\s*push:\s*true\s*\}\)/,
|
|
758
|
+
'backToList should push a clean URL via NexusUrl.update with plan: null',
|
|
750
759
|
);
|
|
751
760
|
assert.ok(
|
|
752
761
|
!/window\.history\.back\s*\(/.test(block[0]),
|
|
@@ -796,7 +805,7 @@ describe('astrolabe.js — deep-link URL state', () => {
|
|
|
796
805
|
);
|
|
797
806
|
assert.ok(block, 'should find renderPlanDetailNotFound body');
|
|
798
807
|
assert.ok(
|
|
799
|
-
!/
|
|
808
|
+
!/NexusUrl\.update/.test(block[0]),
|
|
800
809
|
'renderPlanDetailNotFound must not rewrite the URL',
|
|
801
810
|
);
|
|
802
811
|
assert.match(
|
|
@@ -805,4 +814,52 @@ describe('astrolabe.js — deep-link URL state', () => {
|
|
|
805
814
|
'renderPlanDetailNotFound surfaces a "not found" message',
|
|
806
815
|
);
|
|
807
816
|
});
|
|
817
|
+
|
|
818
|
+
it('list-page status filter writes through NexusUrl.update (replace, no push: true)', () => {
|
|
819
|
+
const writer = astrolabeJs.match(
|
|
820
|
+
/function writeStatusFilterToUrl\(\)[\s\S]*?(?=\n {2}function )/,
|
|
821
|
+
);
|
|
822
|
+
assert.ok(writer, 'writeStatusFilterToUrl should be defined');
|
|
823
|
+
assert.match(
|
|
824
|
+
writer[0],
|
|
825
|
+
/window\.NexusUrl\.update\(\{\s*status:[\s\S]*?\}\s*\)/,
|
|
826
|
+
'status filter must call NexusUrl.update without { push: true }',
|
|
827
|
+
);
|
|
828
|
+
assert.ok(
|
|
829
|
+
!/push:\s*true/.test(writer[0]),
|
|
830
|
+
'status filter writes must use replaceState (D5 default)',
|
|
831
|
+
);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it('readUrlState validates ?status= against STATUS_VALUES and surfaces fail-loud errors (D6)', () => {
|
|
835
|
+
const reader = astrolabeJs.match(
|
|
836
|
+
/function readUrlState\(\)[\s\S]*?(?=\n {2}function )/,
|
|
837
|
+
);
|
|
838
|
+
assert.ok(reader, 'readUrlState should be defined');
|
|
839
|
+
assert.match(
|
|
840
|
+
reader[0],
|
|
841
|
+
/STATUS_VALUES\.indexOf/,
|
|
842
|
+
'readUrlState must validate ?status= against STATUS_VALUES',
|
|
843
|
+
);
|
|
844
|
+
assert.match(
|
|
845
|
+
reader[0],
|
|
846
|
+
/showUrlError\(/,
|
|
847
|
+
'readUrlState must surface invalid values via showUrlError',
|
|
848
|
+
);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('detail-tab state stays non-URL-tracked (D13 narrow reading)', () => {
|
|
852
|
+
// Astrolabe's detail tabs (inventory / scope / decisions /
|
|
853
|
+
// observations / spec) are deliberately NOT URL-tracked. Filter-
|
|
854
|
+
// shaped only — the tab affordance remains client-only state.
|
|
855
|
+
assert.ok(
|
|
856
|
+
!/NexusUrl\.update\(\{\s*tab:/.test(astrolabeJs),
|
|
857
|
+
'detail-tab state must remain client-only (D13)',
|
|
858
|
+
);
|
|
859
|
+
// Defensive — also forbid the legacy updateUrl shape.
|
|
860
|
+
assert.ok(
|
|
861
|
+
!/updateUrl\(\{\s*tab:/.test(astrolabeJs),
|
|
862
|
+
'detail-tab state must remain client-only (D13) — legacy form too',
|
|
863
|
+
);
|
|
864
|
+
});
|
|
808
865
|
});
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
<body>
|
|
9
9
|
<main style="padding: 24px;">
|
|
10
10
|
|
|
11
|
+
<!--
|
|
12
|
+
URL-state validation banner (D6 fail-loud). Shown when the page is
|
|
13
|
+
loaded with an invalid filter value (e.g. ?status=bogus). The banner
|
|
14
|
+
appends one line per invalid value; pages do not silently fall back
|
|
15
|
+
to a default.
|
|
16
|
+
-->
|
|
17
|
+
<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>
|
|
18
|
+
|
|
11
19
|
<!-- ── List View ──────────────────────────────────────────────────── -->
|
|
12
20
|
<div id="plan-list-view">
|
|
13
21
|
<h2>Plans</h2>
|