Skip to main content

max / makenotwork

Delete the content-tab script the described panel replaced static/tab-project-content.js is loaded by nothing: no script tag in any template and no entry in src/shell.rs. Its three surviving wrappers in actions-tabs.js (deselectAll, toggleSelectAllFromEl, sortContent) have no dispatcher site anywhere. The frontend-globals seal drops 161 to 131. The dead-vocabulary seal moves 48 to 49 because the scan is a word match: .table-heading was counted used on a comment inside the deleted script, and the class is written by quasi-webview at request time where no scan of this repo can see it.
Author: Max Johnson <me@maxj.phd> · 2026-09-01 16:15 UTC
Signed with PGP, not checked
Commit: 1edf30418f5e54203973cab22eb7910e5562940b
Parent: b6eabf0
6 files changed, +16 insertions, -357 deletions
M server/build.rs +7 -1
@@ -387,7 +387,13 @@
387 387 /// class that genuinely lost its writer will too. The list the failure prints
388 388 /// is what tells them apart -- if a name on it is one this server still means
389 389 /// to render, that is the bug.
390 - const DEAD_VOCABULARY_HIGH_WATER: usize = 48;
390 + ///
391 + /// 49 rather than 48 because the scan is a word match. `.table-heading` was
392 + /// counted used on the strength of a comment in `static/tab-project-content.js`
393 + /// naming the selector it styled; that script is deleted and the class is
394 + /// emitted by `quasi-webview` at request time, which no scan of this repo can
395 + /// see. A class the description writes is dead here by construction.
396 + const DEAD_VOCABULARY_HIGH_WATER: usize = 49;
391 397
392 398 /// The scripts this build script generates into `static/`.
393 399 ///
@@ -68,19 +68,6 @@
68 68 this.form.requestSubmit();
69 69 };
70 70
71 - // --- project_content.html ---
72 - window.deselectAll = function () {
73 - toggleSelectAll(false);
74 - };
75 -
76 - window.toggleSelectAllFromEl = function () {
77 - toggleSelectAll(this.checked);
78 - };
79 -
80 - window.sortContent = function (colIndex, sortType) {
81 - sortContentTable(parseInt(colIndex, 10), sortType);
82 - };
83 -
84 71 // --- library_collections.html ---
85 72 window.slugifyCollName = function () {
86 73 document.getElementById('new-coll-slug').value = this.value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
@@ -42,7 +42,7 @@
42 42 /// A ratchet that only refuses to grow lets removals go unrecorded, so this
43 43 /// number is a ceiling rather than a progress figure: measure before trusting
44 44 /// it as one.
45 - const HIGH_WATER: usize = 161;
45 + const HIGH_WATER: usize = 131;
46 46
47 47 /// Every file whose global assignments count: the legacy scripts and the typed
48 48 /// modules that replaced them.
@@ -7,8 +7,8 @@
7 7 //!
8 8 //! Compare `routes::pages::dashboard::project_tabs::build_content`, which
9 9 //! answers the same address from Askama when the screen is switched off, and
10 - //! `static/tab-project-content.js`, whose 334 lines are what a described panel
11 - //! replaces.
10 + //! the deleted `static/tab-project-content.js`, whose 334 lines are what a
11 + //! described panel replaced.
12 12 //!
13 13 //! # A view is an address
14 14 //!
@@ -161,8 +161,8 @@
161 161 /// Which rows, in what order, and which bundles are open.
162 162 ///
163 163 /// Query params rather than module state, per quasi's decision 2 and `N12`'s
164 - /// ruling. Every member here is a fact `tab-project-content.js` held in a
165 - /// closure and re-applied by hand after each swap.
164 + /// ruling. Every member here is a fact the deleted `tab-project-content.js`
165 + /// held in a closure and re-applied by hand after each swap.
166 166 #[derive(Debug, Clone, Default, PartialEq, Eq)]
167 167 pub struct View {
168 168 /// What the search box holds. Blank is absent.
@@ -301,8 +301,8 @@
301 301 /// What a narrowing keeps. The ticks a reader made go with the rows they
302 302 /// were on, which is the answer being a new list; `ticked=all` is carried on
303 303 /// the address and would silently come to mean a different everything.
304 - /// `tab-project-content.js` clears the selection on a filter change by hand
305 - /// for the same reason.
304 + /// The deleted `tab-project-content.js` cleared the selection on a filter
305 + /// change by hand for the same reason.
306 306 fn narrowed(&self) -> Self {
307 307 Self {
308 308 ticked: false,
@@ -53,8 +53,8 @@
53 53 //!
54 54 //! Sixteen sites named `#tab-content` and meant one of the panels: four analytics
55 55 //! range buttons, three content refreshes, two members and two subscriptions
56 - //! refreshes inside the monetization composite, four `htmx.ajax` calls in
57 - //! `static/tab-project-content.js`, and three more in the code tab's three JS
56 + //! refreshes inside the monetization composite, four `htmx.ajax` calls in the
57 + //! deleted `static/tab-project-content.js`, and three more in the code tab's JS
58 58 //! files. Under a described strip that id is the strip itself, so each now names
59 59 //! the panel it meant. This is the same class of tail step 2 found three of, and
60 60 //! it is the reason each strip wants a grep before it is described rather than
@@ -1,334 +1,0 @@
1 - (function() {
2 - var __cfg = document.getElementById('tab-project-content-cfg');
3 - var PROJECT_SLUG = __cfg ? __cfg.dataset.projectSlug : '';
4 -
5 - function toggleSelectAll(checked) {
6 - var boxes = document.querySelectorAll('.bulk-check');
7 - for (var i = 0; i < boxes.length; i++) boxes[i].checked = checked;
8 - var selectAll = document.getElementById('select-all');
9 - if (selectAll) selectAll.checked = checked;
10 - updateBulkUI();
11 - }
12 -
13 - function updateBulkUI() {
14 - var checked = document.querySelectorAll('.bulk-check:checked');
15 - var bar = document.getElementById('bulk-action-bar');
16 - var countEl = document.getElementById('bulk-count');
17 - var selectAll = document.getElementById('select-all');
18 - var allBoxes = document.querySelectorAll('.bulk-check');
19 - if (!bar) return;
20 - var hasSelection = checked.length > 0;
21 - bar.classList.toggle('bulk-action-bar--active', hasSelection);
22 - bar.dataset.active = hasSelection ? 'true' : 'false';
23 - countEl.textContent = checked.length + ' selected';
24 - var buttons = bar.querySelectorAll('button');
25 - for (var i = 0; i < buttons.length; i++) buttons[i].disabled = !hasSelection;
26 - if (selectAll) selectAll.checked = allBoxes.length > 0 && checked.length === allBoxes.length;
27 - if (!hasSelection) {
28 - document.getElementById('bulk-price-form').classList.add('hidden');
29 - document.getElementById('bulk-tag-form').classList.add('hidden');
30 - }
31 - }
32 -
33 - function bulkAction(action) {
34 - var checked = document.querySelectorAll('.bulk-check:checked');
35 - if (checked.length === 0) return;
36 -
37 - var titles = [];
38 - for (var i = 0; i < checked.length; i++) {
39 - var row = checked[i].closest('tr');
40 - var link = row ? row.querySelector('td:nth-child(3) a') : null;
41 - if (link) titles.push(link.textContent.trim());
42 - }
43 -
44 - if (action === 'delete') {
45 - var msg = 'Delete ' + checked.length + ' item(s)? This cannot be undone.\n\n' + titles.join('\n');
46 - if (!confirm(msg)) return;
47 - }
48 -
49 - var count = checked.length;
50 - var params = new URLSearchParams();
51 - for (var i = 0; i < checked.length; i++) {
52 - params.append('item_ids', checked[i].value);
53 - }
54 -
55 - fetch('/api/items/bulk/' + action, {
56 - method: 'POST',
57 - headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
58 - body: params.toString()
59 - })
60 - .then(function(r) {
61 - if (!r.ok) return r.text().then(function(t) {
62 - var msg = 'Bulk ' + action + ' failed (' + r.status + ')';
63 - try { var parsed = JSON.parse(t); if (parsed.error) msg = parsed.error; } catch (_) {}
64 - throw new Error(msg);
65 - });
66 - var label = action === 'delete' ? 'deleted' : action === 'publish' ? 'published' : 'unpublished';
67 - showToast(count + ' item(s) ' + label + '.', 'success');
68 - htmx.ajax('GET', '/dashboard/project/' + PROJECT_SLUG + '/tabs/content', '#project-content');
69 - })
70 - .catch(function(err) {
71 - showToast(err.message || 'Bulk operation failed');
72 - });
73 - }
74 -
75 - function quickPublish(itemId) {
76 - var params = new URLSearchParams();
77 - params.append('item_ids', itemId);
78 - fetch('/api/items/bulk/publish', {
79 - method: 'POST',
80 - headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
81 - body: params.toString()
82 - }).then(function(r) {
83 - if (!r.ok) return r.text().then(function(t) {
84 - var msg = 'Publish failed';
85 - try { var parsed = JSON.parse(t); if (parsed.error) msg = parsed.error; } catch (_) {}
86 - throw new Error(msg);
87 - });
88 - showToast('Item published.', 'success');
89 - htmx.ajax('GET', '/dashboard/project/' + PROJECT_SLUG + '/tabs/content', '#project-content');
90 - }).catch(function(err) {
91 - showToast(err.message || 'Publish failed');
92 - });
93 - }
94 -
95 - function showBulkPrice() {
96 - document.getElementById('bulk-price-form').classList.remove('hidden');
97 - document.getElementById('bulk-tag-form').classList.add('hidden');
98 - document.getElementById('bulk-price-input').focus();
99 - }
100 -
101 - function showBulkTag() {
102 - document.getElementById('bulk-tag-form').classList.remove('hidden');
103 - document.getElementById('bulk-price-form').classList.add('hidden');
104 - document.getElementById('bulk-tag-input').focus();
105 - }
106 -
107 - function submitBulkPrice() {
108 - var checked = document.querySelectorAll('.bulk-check:checked');
109 - if (checked.length === 0) return;
110 - var price = document.getElementById('bulk-price-input').value;
111 - if (price === '') { showToast('Enter a price'); return; }
112 -
113 - var params = new URLSearchParams();
114 - for (var i = 0; i < checked.length; i++) params.append('item_ids', checked[i].value);
115 - params.append('price_dollars', price);
116 -
117 - fetch('/api/items/bulk/price', {
118 - method: 'POST',
119 - headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
120 - body: params.toString()
121 - })
122 - .then(function(r) {
123 - if (!r.ok) return r.text().then(function(t) {
124 - var msg = 'Could not update prices. Try again, or refresh the page.';
125 - try { var p = JSON.parse(t); if (p.error) msg = p.error; } catch (_) {}
126 - throw new Error(msg);
127 - });
128 - document.getElementById('bulk-price-form').classList.add('hidden');
129 - htmx.ajax('GET', '/dashboard/project/' + PROJECT_SLUG + '/tabs/content', '#project-content');
130 - })
131 - .catch(function(err) { showToast(err.message || 'Could not update prices. Try again, or refresh the page.'); });
132 - }
133 -
134 - function submitBulkTag() {
135 - var checked = document.querySelectorAll('.bulk-check:checked');
136 - if (checked.length === 0) return;
137 - var tag = document.getElementById('bulk-tag-input').value.trim();
138 - if (!tag) { showToast('Enter a tag.'); return; }
139 -
140 - var params = new URLSearchParams();
141 - for (var i = 0; i < checked.length; i++) params.append('item_ids', checked[i].value);
142 - params.append('tag_slug', tag);
143 -
144 - fetch('/api/items/bulk/tag', {
145 - method: 'POST',
146 - headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
147 - body: params.toString()
148 - })
149 - .then(function(r) {
150 - if (!r.ok) return r.text().then(function(t) {
151 - var msg = 'Could not add the tag. Try again, or refresh the page.';
152 - try { var p = JSON.parse(t); if (p.error) msg = p.error; } catch (_) {}
153 - throw new Error(msg);
154 - });
155 - document.getElementById('bulk-tag-form').classList.add('hidden');
156 - document.getElementById('bulk-tag-input').value = '';
157 - htmx.ajax('GET', '/dashboard/project/' + PROJECT_SLUG + '/tabs/content', '#project-content');
158 - })
159 - .catch(function(err) { showToast(err.message || 'Could not add the tag. Try again, or refresh the page.'); });
160 - }
161 -
162 - function toggleBundleChildren(bundleId) {
163 - var rows = document.querySelectorAll('.bundle-child-' + bundleId);
164 - var btn = document.querySelector('tr:has(.bulk-check[value="' + bundleId + '"]) .bundle-toggle');
165 - var visible = rows.length > 0 && !rows[0].classList.contains('hidden');
166 - for (var i = 0; i < rows.length; i++) {
167 - rows[i].classList.toggle('hidden', visible);
168 - }
169 - if (btn) btn.innerHTML = visible ? '&#9654;' : '&#9660;';
170 - }
171 -
172 - function filterContentTable() {
173 - var query = (document.getElementById('content-search').value || '').toLowerCase();
174 - var status = document.getElementById('content-filter-status').value;
175 - var type = document.getElementById('content-filter-type').value;
176 - var rows = document.querySelectorAll('.data-table tbody > tr:not(.bundle-child)');
177 - for (var i = 0; i < rows.length; i++) {
178 - var row = rows[i];
179 - var title = (row.querySelector('td:nth-child(3)') || {}).textContent || '';
180 - var rowType = (row.querySelector('td:nth-child(4)') || {}).textContent || '';
181 - var rowStatus = row.querySelector('.badge') ? row.querySelector('.badge').textContent.trim() : '';
182 - var matchQuery = !query || title.toLowerCase().indexOf(query) !== -1;
183 - var matchStatus = !status || rowStatus === status;
184 - var matchType = !type || rowType.trim().indexOf(type) !== -1;
185 - var visible = matchQuery && matchStatus && matchType;
186 - row.classList.toggle('hidden', !visible);
187 - var itemCheckbox = row.querySelector('.bulk-check');
188 - if (itemCheckbox && !visible) {
189 - var children = document.querySelectorAll('.bundle-child-' + itemCheckbox.value);
190 - for (var j = 0; j < children.length; j++) {
191 - children[j].classList.add('hidden');
192 - }
193 - }
194 - }
195 - }
196 -
197 - (function() {
198 - var seen = {};
199 - var opts = document.querySelectorAll('.content-type-opt');
200 - for (var i = 0; i < opts.length; i++) {
201 - if (seen[opts[i].value]) { opts[i].remove(); } else { seen[opts[i].value] = true; }
202 - }
203 - })();
204 -
205 - var contentSortState = { col: -1, asc: true };
206 -
207 - function sortContentTable(colIndex, sortType) {
208 - var table = document.querySelector('.data-table');
209 - if (!table) return;
210 - var tbody = table.querySelector('tbody');
211 - var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr:not(.bundle-child)'));
212 -
213 - if (contentSortState.col === colIndex) {
214 - contentSortState.asc = !contentSortState.asc;
215 - } else {
216 - contentSortState.col = colIndex;
217 - contentSortState.asc = true;
218 - }
219 -
220 - function getValue(row) {
221 - var cell = row.cells[colIndex];
222 - if (!cell) return '';
223 - var text = cell.textContent.trim();
224 - if (sortType === 'num') {
225 - return parseInt(text.replace(/[^0-9-]/g, ''), 10) || 0;
226 - }
227 - if (sortType === 'money') {
228 - if (text.toLowerCase() === 'free') return 0;
229 - return parseFloat(text.replace(/[^0-9.-]/g, '')) || 0;
230 - }
231 - return text.toLowerCase();
232 - }
233 -
234 - rows.sort(function(a, b) {
235 - var va = getValue(a);
236 - var vb = getValue(b);
237 - var cmp = 0;
238 - if (typeof va === 'number') {
239 - cmp = va - vb;
240 - } else {
241 - cmp = va < vb ? -1 : va > vb ? 1 : 0;
242 - }
243 - return contentSortState.asc ? cmp : -cmp;
244 - });
245 -
246 - for (var i = 0; i < rows.length; i++) {
247 - tbody.appendChild(rows[i]);
248 - var checkbox = rows[i].querySelector('.bulk-check');
249 - if (checkbox) {
250 - var children = Array.prototype.slice.call(tbody.querySelectorAll('.bundle-child-' + checkbox.value));
251 - for (var j = 0; j < children.length; j++) {
252 - tbody.appendChild(children[j]);
253 - }
254 - }
255 - }
256 -
257 - // One channel, and it is `aria-sort`. The caret is drawn by the generated
258 - // `.table-heading[aria-sort]::after` in static/layout.css, so the arrow and
259 - // the order a screen reader announces cannot disagree. This used to write
260 - // the glyph itself into a `.sort-arrow` span and toggle a modifier class,
261 - // which said nothing to assistive tech and duplicated rules the stylesheet
262 - // already had.
263 - //
264 - // `data-arg` is the column index the heading already carries for the sort
265 - // itself, so the heading is matched by the same number it dispatches on.
266 - var headings = table.querySelectorAll('[data-sortable]');
267 - for (var i = 0; i < headings.length; i++) {
268 - if (parseInt(headings[i].dataset.arg, 10) === colIndex) {
269 - headings[i].setAttribute('aria-sort', contentSortState.asc ? 'ascending' : 'descending');
270 - } else {
271 - headings[i].removeAttribute('aria-sort');
272 - }
273 - }
274 - }
275 -
276 - function inlineRename(itemId) {
277 - var container = document.getElementById('item-title-' + itemId);
278 - var link = container.querySelector('a');
279 - var currentTitle = link.textContent;
280 - var href = link.getAttribute('href');
281 -
282 - var input = document.createElement('input');
283 - input.type = 'text';
284 - input.value = currentTitle;
285 - input.className = 'inline-rename-input';
286 -
287 - function restore(newTitle) {
288 - container.innerHTML = '';
289 - var a = document.createElement('a');
290 - a.href = href;
291 - a.className = 'fw-bold';
292 - a.textContent = newTitle;
293 - container.appendChild(a);
294 - }
295 -
296 - function save() {
297 - var newTitle = input.value.trim();
298 - if (!newTitle || newTitle === currentTitle) { restore(currentTitle); return; }
299 - var form = new FormData();
300 - form.append('title', newTitle);
301 - fetch('/api/items/' + itemId, { method: 'PUT', headers: csrfHeaders(), body: form })
302 - .then(function(r) {
303 - if (!r.ok) throw new Error('Failed');
304 - return r.json();
305 - })
306 - .then(function(data) { restore(data.title); })
307 - .catch(function() { restore(currentTitle); });
308 - }
309 -
310 - input.addEventListener('keydown', function(e) {
311 - if (e.key === 'Enter') { e.preventDefault(); save(); }
312 - if (e.key === 'Escape') { restore(currentTitle); }
313 - });
314 - input.addEventListener('blur', save);
315 -
316 - container.innerHTML = '';
317 - container.appendChild(input);
318 - input.focus();
319 - input.select();
320 - }
321 -
322 - window.toggleSelectAll = toggleSelectAll;
323 - window.updateBulkUI = updateBulkUI;
324 - window.bulkAction = bulkAction;
325 - window.quickPublish = quickPublish;
326 - window.showBulkPrice = showBulkPrice;
327 - window.showBulkTag = showBulkTag;
328 - window.submitBulkPrice = submitBulkPrice;
329 - window.submitBulkTag = submitBulkTag;
330 - window.toggleBundleChildren = toggleBundleChildren;
331 - window.filterContentTable = filterContentTable;
332 - window.sortContentTable = sortContentTable;
333 - window.inlineRename = inlineRename;
334 - })();