@shardworks/spider-apparatus 0.1.235 → 0.1.237
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 +9 -9
- package/src/static/spider-ui.test.ts +148 -45
- package/src/static/spider.js +166 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardworks/spider-apparatus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.237",
|
|
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/stacks-apparatus": "0.1.
|
|
26
|
-
"@shardworks/
|
|
27
|
-
"@shardworks/fabricator-apparatus": "0.1.
|
|
28
|
-
"@shardworks/
|
|
29
|
-
"@shardworks/
|
|
30
|
-
"@shardworks/
|
|
31
|
-
"@shardworks/loom-apparatus": "0.1.
|
|
25
|
+
"@shardworks/stacks-apparatus": "0.1.237",
|
|
26
|
+
"@shardworks/tools-apparatus": "0.1.237",
|
|
27
|
+
"@shardworks/fabricator-apparatus": "0.1.237",
|
|
28
|
+
"@shardworks/animator-apparatus": "0.1.237",
|
|
29
|
+
"@shardworks/codexes-apparatus": "0.1.237",
|
|
30
|
+
"@shardworks/clerk-apparatus": "0.1.237",
|
|
31
|
+
"@shardworks/loom-apparatus": "0.1.237"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "25.5.0",
|
|
35
|
-
"@shardworks/nexus-core": "0.1.
|
|
35
|
+
"@shardworks/nexus-core": "0.1.237"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"dist",
|
|
@@ -20,54 +20,82 @@ const indexHtml = readFileSync(resolve(__dirname, 'index.html'), 'utf-8');
|
|
|
20
20
|
// ── Rig list row structure ──────────────────────────────────────────────
|
|
21
21
|
|
|
22
22
|
describe('spider.js rig list row HTML', () => {
|
|
23
|
+
// After the keyed in-place refactor, rig rows are assembled by
|
|
24
|
+
// createRigRow via document.createElement rather than an HTML template.
|
|
25
|
+
// These assertions pin the same invariants against the new function
|
|
26
|
+
// body: both writ-title and rig-id cells are built as rig-link anchors,
|
|
27
|
+
// and the writ-title cell is never a plain <td> of text.
|
|
28
|
+
const createRigRowMatch = spiderJs.match(
|
|
29
|
+
/function createRigRow\(rig\)[\s\S]*?return tr;\s*\}/,
|
|
30
|
+
);
|
|
31
|
+
const createRigRowBody = createRigRowMatch ? createRigRowMatch[0] : '';
|
|
32
|
+
|
|
23
33
|
it('renders the writ-title cell as a rig-link anchor', () => {
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
const writTitleCellPattern =
|
|
28
|
-
/<td><a class="rig-link" href="#" data-rig-id="[^"]*"\s*>\s*'\s*\+\s*esc\(writTitle\)/;
|
|
34
|
+
// createRigRow builds a dedicated anchor for the writ title with
|
|
35
|
+
// class 'rig-link' and copies data-rig-id across to it.
|
|
36
|
+
assert.ok(createRigRowBody, 'should find createRigRow body');
|
|
29
37
|
assert.match(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'writ-title cell should be a
|
|
38
|
+
createRigRowBody,
|
|
39
|
+
/writTitleAnchor\s*=\s*document\.createElement\(['"]a['"]\)/,
|
|
40
|
+
'writ-title cell should be built around a dedicated anchor element',
|
|
41
|
+
);
|
|
42
|
+
assert.match(
|
|
43
|
+
createRigRowBody,
|
|
44
|
+
/writTitleAnchor\.className\s*=\s*['"]rig-link['"]/,
|
|
45
|
+
'writ-title anchor should carry the rig-link class',
|
|
46
|
+
);
|
|
47
|
+
assert.match(
|
|
48
|
+
createRigRowBody,
|
|
49
|
+
/writTitleAnchor\.setAttribute\(['"]data-rig-id['"]\s*,\s*rig\.id\)/,
|
|
50
|
+
'writ-title anchor should carry data-rig-id for click routing',
|
|
33
51
|
);
|
|
34
52
|
});
|
|
35
53
|
|
|
36
54
|
it('renders the rig-id cell as a rig-link anchor', () => {
|
|
37
|
-
|
|
38
|
-
/<td><a class="rig-link" href="#" data-rig-id="[^"]*"\s*>\s*'\s*\+\s*esc\(rig\.id\)/;
|
|
55
|
+
assert.ok(createRigRowBody, 'should find createRigRow body');
|
|
39
56
|
assert.match(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
'rig-id cell should be a
|
|
57
|
+
createRigRowBody,
|
|
58
|
+
/rigIdAnchor\s*=\s*document\.createElement\(['"]a['"]\)/,
|
|
59
|
+
'rig-id cell should be built around a dedicated anchor element',
|
|
60
|
+
);
|
|
61
|
+
assert.match(
|
|
62
|
+
createRigRowBody,
|
|
63
|
+
/rigIdAnchor\.className\s*=\s*['"]rig-link['"]/,
|
|
64
|
+
'rig-id anchor should carry the rig-link class',
|
|
65
|
+
);
|
|
66
|
+
assert.match(
|
|
67
|
+
createRigRowBody,
|
|
68
|
+
/rigIdAnchor\.textContent\s*=\s*rig\.id/,
|
|
69
|
+
'rig-id anchor should show rig.id as its visible text',
|
|
43
70
|
);
|
|
44
71
|
});
|
|
45
72
|
|
|
46
73
|
it('writ-title and rig-id cells share the same rig-link class', () => {
|
|
47
|
-
// Both cells must use the same class so the click
|
|
48
|
-
// identically. Count rig-link
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
52
|
-
assert.ok(rowTemplateMatch, 'should find the row template block');
|
|
53
|
-
const rowTemplate = rowTemplateMatch[0];
|
|
54
|
-
|
|
55
|
-
const rigLinkCount = (rowTemplate.match(/class="rig-link"/g) || []).length;
|
|
74
|
+
// Both cells must use the same class so the click-wiring treats them
|
|
75
|
+
// identically. Count rig-link references inside createRigRow.
|
|
76
|
+
assert.ok(createRigRowBody, 'should find createRigRow body');
|
|
77
|
+
const rigLinkCount = (createRigRowBody.match(/['"]rig-link['"]/g) || []).length;
|
|
56
78
|
assert.ok(
|
|
57
79
|
rigLinkCount >= 2,
|
|
58
|
-
`expected at least 2 rig-link anchors in
|
|
80
|
+
`expected at least 2 rig-link anchors in createRigRow, found ${rigLinkCount}`,
|
|
59
81
|
);
|
|
60
82
|
});
|
|
61
83
|
|
|
62
84
|
it('writ-title cell is NOT rendered as plain text', () => {
|
|
63
|
-
// Regression guard: the
|
|
64
|
-
//
|
|
85
|
+
// Regression guard: the writ-title cell must always be an anchor, not
|
|
86
|
+
// a plain text <td>. We forbid both the legacy string-template shape
|
|
87
|
+
// and any plain assignment of the writ title onto a <td>'s textContent.
|
|
65
88
|
const plainTitlePattern = /'<td>'\s*\+\s*esc\(writTitle\)\s*\+\s*'<\/td>'/;
|
|
66
89
|
assert.doesNotMatch(
|
|
67
90
|
spiderJs,
|
|
68
91
|
plainTitlePattern,
|
|
69
92
|
'writ-title cell must not be rendered as plain (non-linked) text',
|
|
70
93
|
);
|
|
94
|
+
assert.doesNotMatch(
|
|
95
|
+
spiderJs,
|
|
96
|
+
/writTitleTd\.textContent\s*=\s*writTitle/,
|
|
97
|
+
'writ-title text must never be written directly onto the td',
|
|
98
|
+
);
|
|
71
99
|
});
|
|
72
100
|
|
|
73
101
|
it('writ deep-links target the canonical Clerk writs page path', () => {
|
|
@@ -900,6 +928,70 @@ describe('spider.js pipeline keyed update', () => {
|
|
|
900
928
|
});
|
|
901
929
|
});
|
|
902
930
|
|
|
931
|
+
// ── Rig-list renderer keyed in-place update ─────────────────────────────
|
|
932
|
+
|
|
933
|
+
describe('spider.js rig-list keyed update', () => {
|
|
934
|
+
// Mirrors the pipeline keyed-update block: a fast-path / slow-path
|
|
935
|
+
// update strategy for the rig-tbody, keyed by data-rig-id on each <tr>.
|
|
936
|
+
// The three mutating <td>s carry per-cell class hooks so updateRigRow
|
|
937
|
+
// can patch them without positional selectors.
|
|
938
|
+
|
|
939
|
+
it('createRigRow sets data-rig-id on the <tr>', () => {
|
|
940
|
+
const block = spiderJs.match(
|
|
941
|
+
/function createRigRow\(rig\)[\s\S]*?return tr;\s*\}/,
|
|
942
|
+
);
|
|
943
|
+
assert.ok(block, 'should find createRigRow');
|
|
944
|
+
assert.match(
|
|
945
|
+
block[0],
|
|
946
|
+
/tr\.setAttribute\(['"]data-rig-id['"]\s*,\s*rig\.id\)/,
|
|
947
|
+
'createRigRow should index the row by data-rig-id on the <tr>',
|
|
948
|
+
);
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it('renderRigList has a fast path that patches in place when order is unchanged', () => {
|
|
952
|
+
const block = spiderJs.match(
|
|
953
|
+
/function renderRigList\([\s\S]*?(?=\n \/\/|\n function )/,
|
|
954
|
+
);
|
|
955
|
+
assert.ok(block, 'should find renderRigList');
|
|
956
|
+
assert.match(
|
|
957
|
+
block[0],
|
|
958
|
+
/getAttribute\(['"]data-rig-id['"]\)/,
|
|
959
|
+
'renderRigList should key existing rows by data-rig-id',
|
|
960
|
+
);
|
|
961
|
+
assert.match(
|
|
962
|
+
block[0],
|
|
963
|
+
/orderUnchanged/,
|
|
964
|
+
'renderRigList should compute an orderUnchanged flag for the fast path',
|
|
965
|
+
);
|
|
966
|
+
assert.match(
|
|
967
|
+
block[0],
|
|
968
|
+
/updateRigRow\(/,
|
|
969
|
+
'renderRigList should reach into updateRigRow for in-place patches',
|
|
970
|
+
);
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
it('exposes updateRigRow(row, rig) for in-place cell updates', () => {
|
|
974
|
+
assert.match(
|
|
975
|
+
spiderJs,
|
|
976
|
+
/function updateRigRow\(row, rig\)/,
|
|
977
|
+
'should define updateRigRow(row, rig) helper for in-place row updates',
|
|
978
|
+
);
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
it('rig-row cells carry per-cell class hooks for status, cost, and engines', () => {
|
|
982
|
+
// Without class hooks, the keyed-update path would have to fall back
|
|
983
|
+
// to positional selectors inside the row — brittle and inconsistent
|
|
984
|
+
// with the pipeline pattern.
|
|
985
|
+
for (const cls of ['rig-row-status', 'rig-row-cost', 'rig-row-engines']) {
|
|
986
|
+
assert.match(
|
|
987
|
+
spiderJs,
|
|
988
|
+
new RegExp(`['"]${cls}['"]`),
|
|
989
|
+
`createRigRow/updateRigRow should reference the ${cls} class hook`,
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
|
|
903
995
|
// ── Server-supplied cost (no per-engine fetch) ──────────────────────────
|
|
904
996
|
|
|
905
997
|
describe('spider.js server-supplied engine cost', () => {
|
|
@@ -1015,7 +1107,9 @@ describe('spider.js rig-list Cost column', () => {
|
|
|
1015
1107
|
// T5 / D7: the rig-list table has a dedicated Cost column, rendered
|
|
1016
1108
|
// immediately to the left of Engines, sourced from rig.costSummary.
|
|
1017
1109
|
// The Cost column always renders — showing $0.00 when no sessions have
|
|
1018
|
-
// reported cost yet.
|
|
1110
|
+
// reported cost yet. After the keyed in-place refactor, the Cost cell
|
|
1111
|
+
// is structurally built in createRigRow and the $0.00 fallback lives
|
|
1112
|
+
// inside updateRigRow.
|
|
1019
1113
|
|
|
1020
1114
|
it('index.html declares a Cost <th> between Writ Title and Engines', () => {
|
|
1021
1115
|
assert.match(
|
|
@@ -1026,19 +1120,20 @@ describe('spider.js rig-list Cost column', () => {
|
|
|
1026
1120
|
});
|
|
1027
1121
|
|
|
1028
1122
|
it('row template emits a cost cell between writ-title and engines cells', () => {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
const
|
|
1037
|
-
const
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
assert.ok(
|
|
1041
|
-
assert.ok(
|
|
1123
|
+
// createRigRow appends cells to the <tr> in DOM order, so we pin the
|
|
1124
|
+
// ordering invariant by checking that the writ-title cell is appended
|
|
1125
|
+
// before the cost cell, which is appended before the engines cell.
|
|
1126
|
+
const createRigRowMatch = spiderJs.match(
|
|
1127
|
+
/function createRigRow\(rig\)[\s\S]*?return tr;\s*\}/,
|
|
1128
|
+
);
|
|
1129
|
+
assert.ok(createRigRowMatch, 'should find createRigRow body');
|
|
1130
|
+
const body = createRigRowMatch[0];
|
|
1131
|
+
const writIdx = body.indexOf('tr.appendChild(writTitleTd)');
|
|
1132
|
+
const costIdx = body.indexOf('tr.appendChild(costTd)');
|
|
1133
|
+
const enginesIdx = body.indexOf('tr.appendChild(enginesTd)');
|
|
1134
|
+
assert.ok(writIdx >= 0, 'createRigRow should append a writ-title cell');
|
|
1135
|
+
assert.ok(costIdx >= 0, 'createRigRow should append a cost cell');
|
|
1136
|
+
assert.ok(enginesIdx >= 0, 'createRigRow should append an engines cell');
|
|
1042
1137
|
assert.ok(
|
|
1043
1138
|
writIdx < costIdx && costIdx < enginesIdx,
|
|
1044
1139
|
'cost cell must sit between writ-title and engines cells',
|
|
@@ -1046,15 +1141,23 @@ describe('spider.js rig-list Cost column', () => {
|
|
|
1046
1141
|
});
|
|
1047
1142
|
|
|
1048
1143
|
it('row template falls back to 0 when costSummary is absent (renders $0.00)', () => {
|
|
1049
|
-
|
|
1050
|
-
|
|
1144
|
+
// The $0.00 fallback now lives in updateRigRow — the cost cell's
|
|
1145
|
+
// textContent is always derived via formatCostUsd, defaulting costUsd
|
|
1146
|
+
// to 0 when costSummary is missing or costUsd is not a number.
|
|
1147
|
+
const updateRigRowMatch = spiderJs.match(
|
|
1148
|
+
/function updateRigRow\(row, rig\)[\s\S]*?(?=\n \}\n\n \/\/|\n function )/,
|
|
1051
1149
|
);
|
|
1052
|
-
assert.ok(
|
|
1053
|
-
const
|
|
1150
|
+
assert.ok(updateRigRowMatch, 'should find updateRigRow body');
|
|
1151
|
+
const body = updateRigRowMatch[0];
|
|
1054
1152
|
assert.match(
|
|
1055
|
-
|
|
1153
|
+
body,
|
|
1056
1154
|
/rig\.costSummary[\s\S]*?costUsd[\s\S]*?:\s*0/,
|
|
1057
|
-
'
|
|
1155
|
+
'updateRigRow should default costUsd to 0 when costSummary is missing',
|
|
1156
|
+
);
|
|
1157
|
+
assert.match(
|
|
1158
|
+
body,
|
|
1159
|
+
/formatCostUsd\(costUsd\)/,
|
|
1160
|
+
'updateRigRow should render the cell via formatCostUsd',
|
|
1058
1161
|
);
|
|
1059
1162
|
});
|
|
1060
1163
|
});
|
package/src/static/spider.js
CHANGED
|
@@ -577,35 +577,174 @@
|
|
|
577
577
|
if (empty) empty.style.display = 'none';
|
|
578
578
|
if (table) table.style.display = '';
|
|
579
579
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
var
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
580
|
+
// Index existing row children by rig id (mirrors renderPipelineInto).
|
|
581
|
+
var existingRows = {};
|
|
582
|
+
var existingList = tbody.children;
|
|
583
|
+
for (var i = 0; i < existingList.length; i++) {
|
|
584
|
+
var existingId = existingList[i].getAttribute('data-rig-id');
|
|
585
|
+
if (existingId) existingRows[existingId] = existingList[i];
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Fast path: same rig set in the same order. Patch each row in place
|
|
589
|
+
// without touching the parent's child list — the common case during
|
|
590
|
+
// the 2 s poll when nothing has been added or removed.
|
|
591
|
+
var orderUnchanged =
|
|
592
|
+
existingList.length === filtered.length &&
|
|
593
|
+
filtered.every(function (rig, idx) {
|
|
594
|
+
return existingList[idx].getAttribute('data-rig-id') === rig.id;
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
if (orderUnchanged) {
|
|
598
|
+
for (var f = 0; f < filtered.length; f++) {
|
|
599
|
+
updateRigRow(existingList[f], filtered[f]);
|
|
600
|
+
}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Slow path: rig set or order changed. Detach every row (without
|
|
605
|
+
// `innerHTML = ''`, which would drop the nodes we want to reuse),
|
|
606
|
+
// then re-append matched rows via createRigRow / updateRigRow in the
|
|
607
|
+
// new filtered order. Rows for rig ids not in the incoming list are
|
|
608
|
+
// simply not re-attached and fall out of scope.
|
|
609
|
+
while (tbody.firstChild) {
|
|
610
|
+
tbody.removeChild(tbody.firstChild);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
filtered.forEach(function (rig) {
|
|
614
|
+
var row = existingRows[rig.id];
|
|
615
|
+
if (!row) {
|
|
616
|
+
row = createRigRow(rig);
|
|
617
|
+
}
|
|
618
|
+
updateRigRow(row, rig);
|
|
619
|
+
tbody.appendChild(row);
|
|
594
620
|
});
|
|
621
|
+
}
|
|
595
622
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
623
|
+
// ── Rig-row construction + in-place update ─────────────────────────────
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Build a new <tr> for the given rig. Modeled one-for-one on
|
|
627
|
+
* createPipelineNode: data-rig-id on the outer node (D3), per-cell class
|
|
628
|
+
* hooks on the mutating <td>s (D4), and rig-link click handlers wired
|
|
629
|
+
* once per anchor with a closure over rig.id (D5/D6). The live rig is
|
|
630
|
+
* resolved inside the handler via rigs.find so reused rows pick up the
|
|
631
|
+
* latest payload without needing their listeners re-attached.
|
|
632
|
+
*/
|
|
633
|
+
function createRigRow(rig) {
|
|
634
|
+
var tr = document.createElement('tr');
|
|
635
|
+
tr.setAttribute('data-rig-id', rig.id);
|
|
636
|
+
|
|
637
|
+
// Cell 1: Status. A stable <span class="badge"> child is patched in
|
|
638
|
+
// place by updateRigRow — the className toggles the variant, the
|
|
639
|
+
// textContent is the status word.
|
|
640
|
+
var statusTd = document.createElement('td');
|
|
641
|
+
statusTd.className = 'rig-row-status';
|
|
642
|
+
var statusBadge = document.createElement('span');
|
|
643
|
+
statusBadge.className = 'badge';
|
|
644
|
+
statusTd.appendChild(statusBadge);
|
|
645
|
+
tr.appendChild(statusTd);
|
|
646
|
+
|
|
647
|
+
// Cell 2: Writ title (rig-link anchor). Writ title text is written by
|
|
648
|
+
// updateRigRow on every poll (D9) so writLookup refreshes are visible.
|
|
649
|
+
var writTitleTd = document.createElement('td');
|
|
650
|
+
var writTitleAnchor = document.createElement('a');
|
|
651
|
+
writTitleAnchor.className = 'rig-link';
|
|
652
|
+
writTitleAnchor.href = '#';
|
|
653
|
+
writTitleAnchor.setAttribute('data-rig-id', rig.id);
|
|
654
|
+
writTitleAnchor.addEventListener('click', function (e) {
|
|
655
|
+
e.preventDefault();
|
|
656
|
+
var live = rigs.find(function (r) { return r.id === rig.id; });
|
|
657
|
+
if (live) showRigDetail(live);
|
|
658
|
+
});
|
|
659
|
+
writTitleTd.appendChild(writTitleAnchor);
|
|
660
|
+
tr.appendChild(writTitleTd);
|
|
661
|
+
|
|
662
|
+
// Cell 3: Cost.
|
|
663
|
+
var costTd = document.createElement('td');
|
|
664
|
+
costTd.className = 'rig-row-cost';
|
|
665
|
+
tr.appendChild(costTd);
|
|
666
|
+
|
|
667
|
+
// Cell 4: Engines.
|
|
668
|
+
var enginesTd = document.createElement('td');
|
|
669
|
+
enginesTd.className = 'rig-row-engines';
|
|
670
|
+
tr.appendChild(enginesTd);
|
|
671
|
+
|
|
672
|
+
// Cell 5: Rig id (rig-link anchor). The id text never changes for a
|
|
673
|
+
// given row, so it's written once at create time.
|
|
674
|
+
var rigIdTd = document.createElement('td');
|
|
675
|
+
var rigIdAnchor = document.createElement('a');
|
|
676
|
+
rigIdAnchor.className = 'rig-link';
|
|
677
|
+
rigIdAnchor.href = '#';
|
|
678
|
+
rigIdAnchor.setAttribute('data-rig-id', rig.id);
|
|
679
|
+
rigIdAnchor.textContent = rig.id;
|
|
680
|
+
rigIdAnchor.addEventListener('click', function (e) {
|
|
681
|
+
e.preventDefault();
|
|
682
|
+
var live = rigs.find(function (r) { return r.id === rig.id; });
|
|
683
|
+
if (live) showRigDetail(live);
|
|
684
|
+
});
|
|
685
|
+
rigIdTd.appendChild(rigIdAnchor);
|
|
686
|
+
tr.appendChild(rigIdTd);
|
|
687
|
+
|
|
688
|
+
// Cell 6: Writ deep-link to the Clerk writs page. Stable per row.
|
|
689
|
+
var writIdTd = document.createElement('td');
|
|
690
|
+
var writIdAnchor = document.createElement('a');
|
|
691
|
+
writIdAnchor.href = '/pages/writs/?writ=' + encodeURIComponent(rig.writId || '');
|
|
692
|
+
writIdAnchor.textContent = rig.writId || '';
|
|
693
|
+
writIdTd.appendChild(writIdAnchor);
|
|
694
|
+
tr.appendChild(writIdTd);
|
|
695
|
+
|
|
696
|
+
// Cell 7: Created timestamp (stable per row).
|
|
697
|
+
var createdTd = document.createElement('td');
|
|
698
|
+
createdTd.textContent = formatDate(rig.createdAt);
|
|
699
|
+
tr.appendChild(createdTd);
|
|
700
|
+
|
|
701
|
+
return tr;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Patch the mutating cells of an existing rig row in place. Safe to
|
|
706
|
+
* call on every 2 s poll — writes only happen when values actually
|
|
707
|
+
* changed, mirroring the idempotent-write pattern used by setText /
|
|
708
|
+
* updatePipelineNode. Cells touched: status (badge class + text), writ
|
|
709
|
+
* title (anchor textContent — re-read from writLookup each call per
|
|
710
|
+
* D9), cost, engines.
|
|
711
|
+
*/
|
|
712
|
+
function updateRigRow(row, rig) {
|
|
713
|
+
// Status badge — class + text, both idempotent.
|
|
714
|
+
var statusTd = row.querySelector('.rig-row-status');
|
|
715
|
+
if (statusTd) {
|
|
716
|
+
var badgeEl = statusTd.querySelector('.badge');
|
|
717
|
+
if (badgeEl) {
|
|
718
|
+
var bc = badgeClass(rig.status);
|
|
719
|
+
var nextClass = bc ? 'badge ' + bc : 'badge';
|
|
720
|
+
if (badgeEl.className !== nextClass) badgeEl.className = nextClass;
|
|
721
|
+
if (badgeEl.textContent !== rig.status) badgeEl.textContent = rig.status;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Writ title — re-read from writLookup on every call (D9). The
|
|
726
|
+
// writ-title anchor is the first .rig-link in the row; the rig-id
|
|
727
|
+
// anchor appears later and is not touched here.
|
|
728
|
+
var writTitle = (writLookup[rig.writId] && writLookup[rig.writId].title) || '\u2014';
|
|
729
|
+
var writTitleAnchor = row.querySelector('.rig-link');
|
|
730
|
+
if (writTitleAnchor && writTitleAnchor.textContent !== writTitle) {
|
|
731
|
+
writTitleAnchor.textContent = writTitle;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Cost — D7/D11 fallback to 0 when costSummary is absent so the cell
|
|
735
|
+
// always renders $0.00 rather than blank.
|
|
736
|
+
var costTd = row.querySelector('.rig-row-cost');
|
|
737
|
+
if (costTd) {
|
|
738
|
+
var costUsd = (rig.costSummary && typeof rig.costSummary.costUsd === 'number') ? rig.costSummary.costUsd : 0;
|
|
739
|
+
var costText = formatCostUsd(costUsd);
|
|
740
|
+
if (costTd.textContent !== costText) costTd.textContent = costText;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Engine summary.
|
|
744
|
+
var enginesTd = row.querySelector('.rig-row-engines');
|
|
745
|
+
if (enginesTd) {
|
|
746
|
+
var enginesText = engineSummary(rig.engines);
|
|
747
|
+
if (enginesTd.textContent !== enginesText) enginesTd.textContent = enginesText;
|
|
609
748
|
}
|
|
610
749
|
}
|
|
611
750
|
|