Skip to main content

max / makenotwork

Extract item-details.js: bundle, section, tag management Move ~350 lines of inline JavaScript from item_details.html into static/item-details.js. Uses data-item-id attribute to pass server data and htmx:afterSwap to re-initialize on HTMX tab swap. Removes 4 inline script blocks (bundle management, section CRUD, AI tier toggle, tag search). All functionality preserved. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-05 19:23 UTC
Commit: 76916c6a7640e5bdb723d23ca50b4e4f4a4a63aa
Parent: 8379304
4 files changed, +330 insertions, -285 deletions
@@ -36,7 +36,7 @@ Dashboard restructure complete (Phases 1-6 in todo_done.md). Tab layout: Project
36 36 - [x] Replace `window.location.reload()` with HTMX tab re-fetch on item settings (4x), item details (1x), user profile (2x)
37 37 - [x] Remove "Loading..." placeholder text from 2FA and passkey sections (empty until revealed)
38 38 - [x] Add static JS convention to CONTRIBUTING.md — data-attribute bridges, IIFE pattern, htmx:afterSwap re-init, CLAUDE.md updated
39 - - [ ] Extract item-details.js (~350 lines) — bundle, section, tag management from item_details.html. Uses htmx:afterSwap + event delegation
39 + - [x] Extract item-details.js (~350 lines) — bundle, section, tag management from item_details.html. Uses htmx:afterSwap for re-init on tab swap
40 40 - [ ] Extract item-upload.js (~220 lines) — merge audio + version upload from partials. Same re-init pattern
41 41 - [ ] Extract blog-editor.js (~100 lines) — from dashboard-blog-editor.html. Full page, no re-init needed
42 42 - [ ] (Deferred) Extract audio-player.js (~450 lines) — complex state machine, full page load only, low priority
@@ -0,0 +1,323 @@
1 + /**
2 + * Item details tab: bundle management, section editing, tag search.
3 + *
4 + * Loaded once in dashboard-item.html. Re-initializes on HTMX tab swap
5 + * via htmx:afterSwap. Reads item ID from data-item-id on the container.
6 + */
7 + (function() {
8 + function init() {
9 + var container = document.getElementById('item-details-tab');
10 + if (!container) return;
11 + var itemId = container.dataset.itemId;
12 + if (!itemId) return;
13 +
14 + initBundles(itemId);
15 + initSections(itemId);
16 + initTagSearch(itemId);
17 + initAiTierToggle();
18 + }
19 +
20 + // ── Bundles ──
21 +
22 + function initBundles(bundleId) {
23 + var addBtn = document.getElementById('bundle-add-btn');
24 + if (!addBtn) return;
25 +
26 + addBtn.addEventListener('click', function() {
27 + var select = document.getElementById('bundle-add-select');
28 + var itemId = select.value;
29 + if (!itemId) return;
30 + var label = select.options[select.selectedIndex].text;
31 + this.disabled = true;
32 +
33 + fetch('/api/items/' + bundleId + '/bundle/add', {
34 + method: 'POST',
35 + headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
36 + body: JSON.stringify({ item_id: itemId })
37 + })
38 + .then(function(res) {
39 + if (!res.ok) throw new Error('Failed to add');
40 + select.remove(select.selectedIndex);
41 + select.value = '';
42 + var empty = document.getElementById('bundle-empty');
43 + if (empty) empty.remove();
44 +
45 + var titleMatch = label.match(/^(.+)\s+\((.+)\)$/);
46 + var title = titleMatch ? titleMatch[1] : label;
47 + var type = titleMatch ? titleMatch[2] : '';
48 +
49 + var row = document.createElement('div');
50 + row.className = 'bundle-row';
51 + row.dataset.childId = itemId;
52 + row.style.cssText = 'display:flex;align-items:center;gap:0.75rem;padding:0.6rem 0;border-bottom:1px solid var(--border);';
53 + row.innerHTML =
54 + '<span style="font-size:0.8rem;padding:0.2rem 0.6rem;background:var(--surface-muted);white-space:nowrap;">' + escapeHtml(type) + '</span>' +
55 + '<span style="flex:1;">' + escapeHtml(title) + '</span>' +
56 + '<label style="font-size:0.8rem;display:flex;align-items:center;gap:0.25rem;cursor:pointer;">' +
57 + '<input type="checkbox" class="bundle-listed-toggle" data-child-id="' + itemId + '"> Unlisted</label>' +
58 + '<button type="button" class="secondary bundle-remove-btn" data-child-id="' + itemId + '" style="padding:0.25rem 0.6rem;font-size:0.8rem;">Remove</button>';
59 + document.getElementById('bundle-items-list').appendChild(row);
60 + updateBundleCount(1);
61 + attachBundleRowHandlers(row, bundleId);
62 + })
63 + .catch(function(err) { showToast(err.message); })
64 + .finally(function() { addBtn.disabled = false; });
65 + });
66 +
67 + document.querySelectorAll('.bundle-row').forEach(function(row) {
68 + attachBundleRowHandlers(row, bundleId);
69 + });
70 + }
71 +
72 + function attachBundleRowHandlers(row, bundleId) {
73 + row.querySelector('.bundle-remove-btn')?.addEventListener('click', function() {
74 + var childId = this.dataset.childId;
75 + fetch('/api/items/' + bundleId + '/bundle/' + childId, {
76 + method: 'DELETE',
77 + headers: csrfHeaders()
78 + })
79 + .then(function(res) {
80 + if (!res.ok) throw new Error('Failed to remove');
81 + var title = row.querySelector('span[style*="flex"]').textContent;
82 + var type = row.querySelector('span[style*="surface-muted"]').textContent;
83 + var select = document.getElementById('bundle-add-select');
84 + if (select) {
85 + var opt = document.createElement('option');
86 + opt.value = childId;
87 + opt.textContent = title + ' (' + type + ')';
88 + select.appendChild(opt);
89 + }
90 + row.remove();
91 + updateBundleCount(-1);
92 + })
93 + .catch(function(err) { showToast(err.message); });
94 + });
95 +
96 + row.querySelector('.bundle-listed-toggle')?.addEventListener('change', function() {
97 + var childId = this.dataset.childId;
98 + var listed = !this.checked;
99 + fetch('/api/items/' + bundleId + '/bundle/' + childId + '/listed', {
100 + method: 'PUT',
101 + headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
102 + body: JSON.stringify({ listed: listed })
103 + })
104 + .catch(function(err) { showToast(err.message); });
105 + });
106 + }
107 +
108 + function updateBundleCount(delta) {
109 + var el = document.getElementById('bundle-count');
110 + if (el) el.textContent = parseInt(el.textContent) + delta;
111 + }
112 +
113 + // ── Sections ──
114 +
115 + function initSections(itemId) {
116 + var addBtn = document.getElementById('add-sec-btn');
117 + if (!addBtn) return;
118 +
119 + addBtn.addEventListener('click', function() {
120 + var title = document.getElementById('new-sec-title').value.trim();
121 + var body = document.getElementById('new-sec-body').value;
122 + var status = document.getElementById('sec-add-status');
123 + if (!title) { status.textContent = 'Title is required'; return; }
124 + this.disabled = true;
125 + status.textContent = '';
126 +
127 + fetch('/api/items/' + itemId + '/sections', {
128 + method: 'POST',
129 + headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
130 + body: JSON.stringify({ title: title, body: body })
131 + })
132 + .then(function(res) {
133 + if (!res.ok) return res.json().then(function(d) { throw new Error(d.error || 'Failed'); });
134 + return res.json();
135 + })
136 + .then(function(sec) {
137 + var empty = document.getElementById('sections-empty');
138 + if (empty) empty.remove();
139 + var row = document.createElement('div');
140 + row.className = 'section-mgmt-row';
141 + row.dataset.id = sec.id;
142 + row.style.cssText = 'display:flex;align-items:center;gap:0.75rem;padding:0.6rem 0;border-bottom:1px solid var(--border);';
143 + row.innerHTML =
144 + '<span style="flex:1;font-weight:bold;">' + escapeHtml(sec.title) + '</span>' +
145 + '<span style="font-size:0.8rem;opacity:0.6;">' + (sec.body || '').length + ' chars</span>' +
146 + '<button type="button" class="secondary section-edit-btn" data-id="' + sec.id + '" style="padding:0.25rem 0.6rem;font-size:0.8rem;">Edit</button>' +
147 + '<button type="button" class="secondary section-del-btn" data-id="' + sec.id + '" style="padding:0.25rem 0.6rem;font-size:0.8rem;">Delete</button>';
148 + document.getElementById('sections-list').appendChild(row);
149 + attachSectionRowHandlers(row, itemId);
150 + updateSectionCount(1);
151 + document.getElementById('new-sec-title').value = '';
152 + document.getElementById('new-sec-body').value = '';
153 + document.getElementById('section-add-details').removeAttribute('open');
154 + })
155 + .catch(function(err) { status.textContent = err.message; })
156 + .finally(function() { addBtn.disabled = false; });
157 + });
158 +
159 + document.getElementById('save-sec-btn').addEventListener('click', function() {
160 + var id = document.getElementById('edit-sec-id').value;
161 + var title = document.getElementById('edit-sec-title').value.trim();
162 + var body = document.getElementById('edit-sec-body').value;
163 + var status = document.getElementById('sec-edit-status');
164 + if (!title) { status.textContent = 'Title is required'; return; }
165 + this.disabled = true;
166 +
167 + fetch('/api/sections/' + id, {
168 + method: 'PUT',
169 + headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
170 + body: JSON.stringify({ title: title, body: body })
171 + })
172 + .then(function(res) {
173 + if (!res.ok) return res.json().then(function(d) { throw new Error(d.error || 'Failed'); });
174 + return res.json();
175 + })
176 + .then(function(sec) {
177 + var row = document.querySelector('.section-mgmt-row[data-id="' + id + '"]');
178 + if (row) {
179 + row.querySelector('span[style*="font-weight"]').textContent = sec.title;
180 + row.querySelector('span[style*="opacity"]').textContent = (sec.body || '').length + ' chars';
181 + }
182 + document.getElementById('section-edit-modal').style.display = 'none';
183 + })
184 + .catch(function(err) { status.textContent = err.message; })
185 + .finally(function() { document.getElementById('save-sec-btn').disabled = false; });
186 + });
187 +
188 + document.getElementById('cancel-sec-btn').addEventListener('click', function() {
189 + document.getElementById('section-edit-modal').style.display = 'none';
190 + });
191 +
192 + document.querySelectorAll('.section-mgmt-row').forEach(function(row) {
193 + attachSectionRowHandlers(row, itemId);
194 + });
195 + }
196 +
197 + function attachSectionRowHandlers(row, itemId) {
198 + row.querySelector('.section-del-btn').addEventListener('click', function() {
199 + var id = this.dataset.id;
200 + if (!confirm('Delete this section?')) return;
201 + fetch('/api/sections/' + id, { method: 'DELETE', headers: csrfHeaders() })
202 + .then(function(res) { if (res.ok) { row.remove(); updateSectionCount(-1); } else showToast('Failed to delete'); })
203 + .catch(function() { showToast('Failed to delete'); });
204 + });
205 +
206 + row.querySelector('.section-edit-btn').addEventListener('click', function() {
207 + var id = this.dataset.id;
208 + var modal = document.getElementById('section-edit-modal');
209 + document.getElementById('edit-sec-id').value = id;
210 + document.getElementById('edit-sec-title').value = row.querySelector('span[style*="font-weight"]').textContent;
211 + document.getElementById('edit-sec-body').value = '';
212 + document.getElementById('sec-edit-status').textContent = 'Loading...';
213 + modal.style.display = 'block';
214 +
215 + fetch('/api/items/' + itemId + '/sections')
216 + .then(function(r) { return r.json(); })
217 + .then(function(resp) {
218 + var sec = resp.data.find(function(s) { return s.id === id; });
219 + if (sec) {
220 + document.getElementById('edit-sec-title').value = sec.title;
221 + document.getElementById('edit-sec-body').value = sec.body;
222 + }
223 + document.getElementById('sec-edit-status').textContent = '';
224 + })
225 + .catch(function() { document.getElementById('sec-edit-status').textContent = 'Failed to load'; });
226 + });
227 + }
228 +
229 + function updateSectionCount(delta) {
230 + var el = document.getElementById('section-count');
231 + if (el) el.textContent = parseInt(el.textContent) + delta;
232 + }
233 +
234 + // ── Tag Search ──
235 +
236 + function initTagSearch(itemId) {
237 + var input = document.getElementById('item-tags');
238 + if (!input) return;
239 +
240 + var tagSearchTimeout;
241 +
242 + window.searchTags = function(q) {
243 + clearTimeout(tagSearchTimeout);
244 + var suggestions = document.getElementById('tag-suggestions');
245 + if (!q.trim()) { suggestions.style.display = 'none'; return; }
246 + tagSearchTimeout = setTimeout(function() {
247 + fetch('/api/tags/search?q=' + encodeURIComponent(q))
248 + .then(function(r) { return r.json(); })
249 + .then(function(tags) {
250 + if (!tags.length) { suggestions.style.display = 'none'; return; }
251 + suggestions.innerHTML = '';
252 + tags.forEach(function(t) {
253 + var div = document.createElement('div');
254 + div.style.cssText = 'padding: 0.4rem 0.6rem; cursor: pointer; font-size: 0.85rem;';
255 + div.textContent = t.name;
256 + div.addEventListener('mouseover', function() { this.style.background = 'var(--border)'; });
257 + div.addEventListener('mouseout', function() { this.style.background = 'transparent'; });
258 + div.addEventListener('click', function() { addTagById(t.id); });
259 + suggestions.appendChild(div);
260 + });
261 + suggestions.style.display = 'block';
262 + })
263 + .catch(function() { suggestions.style.display = 'none'; });
264 + }, 200);
265 + };
266 +
267 + function addTagById(tagId) {
268 + var form = new FormData();
269 + form.append('tag_id', tagId);
270 + fetch('/api/items/' + itemId + '/tags', { method: 'POST', headers: csrfHeaders(), body: form })
271 + .then(function(r) { return r.text(); })
272 + .then(function(html) {
273 + if (html) {
274 + document.getElementById('tags-container').insertAdjacentHTML('beforeend', html);
275 + }
276 + document.getElementById('item-tags').value = '';
277 + document.getElementById('tag-suggestions').style.display = 'none';
278 + htmx.process(document.getElementById('tags-container'));
279 + })
280 + .catch(function() {
281 + var msg = document.getElementById('item-save-status');
282 + if (msg) msg.textContent = 'Failed to add tag. Please try again.';
283 + });
284 + }
285 +
286 + document.addEventListener('click', function(e) {
287 + if (!e.target.closest('#item-tags') && !e.target.closest('#tag-suggestions')) {
288 + var el = document.getElementById('tag-suggestions');
289 + if (el) el.style.display = 'none';
290 + }
291 + });
292 + }
293 +
294 + // ── AI Tier Toggle ──
295 +
296 + function initAiTierToggle() {
297 + var tierSelect = document.getElementById('ai_tier');
298 + var disclosureRow = document.getElementById('ai-disclosure-row');
299 + if (tierSelect && disclosureRow) {
300 + tierSelect.addEventListener('change', function() {
301 + disclosureRow.style.display = this.value === 'assisted' ? '' : 'none';
302 + });
303 + }
304 + }
305 +
306 + // ── Shared ──
307 +
308 + function escapeHtml(s) {
309 + var d = document.createElement('div');
310 + d.textContent = s;
311 + return d.innerHTML;
312 + }
313 +
314 + // Run on initial load
315 + init();
316 +
317 + // Re-run when HTMX swaps in the details tab
318 + document.body.addEventListener('htmx:afterSwap', function(e) {
319 + if (e.detail.target.id === 'tab-content') {
320 + init();
321 + }
322 + });
323 + })();
@@ -132,3 +132,7 @@
132 132 </div>
133 133 </div>
134 134 {% endblock %}
135 +
136 + {% block scripts %}
137 + <script src="/static/item-details.js"></script>
138 + {% endblock %}
@@ -1,4 +1,5 @@
1 1 <!-- Basic Information -->
2 + <div id="item-details-tab" data-item-id="{{ item.id }}">
2 3 <div class="content-section">
3 4 <div class="section-header">
4 5 <h2>Basic Information</h2>
@@ -167,101 +168,6 @@
167 168 {% endif %}
168 169 </div>
169 170
170 - <script>
171 - (function() {
172 - var bundleId = '{{ item.id }}';
173 -
174 - document.getElementById('bundle-add-btn')?.addEventListener('click', function() {
175 - var select = document.getElementById('bundle-add-select');
176 - var itemId = select.value;
177 - if (!itemId) return;
178 - var label = select.options[select.selectedIndex].text;
179 - this.disabled = true;
180 -
181 - fetch('/api/items/' + bundleId + '/bundle/add', {
182 - method: 'POST',
183 - headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
184 - body: JSON.stringify({ item_id: itemId })
185 - })
186 - .then(function(res) {
187 - if (!res.ok) throw new Error('Failed to add');
188 - select.remove(select.selectedIndex);
189 - select.value = '';
190 - var empty = document.getElementById('bundle-empty');
191 - if (empty) empty.remove();
192 -
193 - var titleMatch = label.match(/^(.+)\s+\((.+)\)$/);
194 - var title = titleMatch ? titleMatch[1] : label;
195 - var type = titleMatch ? titleMatch[2] : '';
196 -
197 - var row = document.createElement('div');
198 - row.className = 'bundle-row';
199 - row.dataset.childId = itemId;
200 - row.style.cssText = 'display:flex;align-items:center;gap:0.75rem;padding:0.6rem 0;border-bottom:1px solid var(--border);';
201 - row.innerHTML =
202 - '<span style="font-size:0.8rem;padding:0.2rem 0.6rem;background:var(--surface-muted);white-space:nowrap;">' + escapeHtml(type) + '</span>' +
203 - '<span style="flex:1;">' + escapeHtml(title) + '</span>' +
204 - '<label style="font-size:0.8rem;display:flex;align-items:center;gap:0.25rem;cursor:pointer;">' +
205 - '<input type="checkbox" class="bundle-listed-toggle" data-child-id="' + itemId + '"> Unlisted</label>' +
206 - '<button type="button" class="secondary bundle-remove-btn" data-child-id="' + itemId + '" style="padding:0.25rem 0.6rem;font-size:0.8rem;">Remove</button>';
207 - document.getElementById('bundle-items-list').appendChild(row);
208 - updateCount(1);
209 - attachRowHandlers(row);
210 - })
211 - .catch(function(err) { showToast(err.message); })
212 - .finally(function() { document.getElementById('bundle-add-btn').disabled = false; });
213 - });
214 -
215 - function attachRowHandlers(row) {
216 - row.querySelector('.bundle-remove-btn')?.addEventListener('click', function() {
217 - var childId = this.dataset.childId;
218 - fetch('/api/items/' + bundleId + '/bundle/' + childId, {
219 - method: 'DELETE',
220 - headers: csrfHeaders()
221 - })
222 - .then(function(res) {
223 - if (!res.ok) throw new Error('Failed to remove');
224 - var title = row.querySelector('span[style*="flex"]').textContent;
225 - var type = row.querySelector('span[style*="surface-muted"]').textContent;
226 - var select = document.getElementById('bundle-add-select');
227 - if (select) {
228 - var opt = document.createElement('option');
229 - opt.value = childId;
230 - opt.textContent = title + ' (' + type + ')';
231 - select.appendChild(opt);
232 - }
233 - row.remove();
234 - updateCount(-1);
235 - })
236 - .catch(function(err) { showToast(err.message); });
237 - });
238 -
239 - row.querySelector('.bundle-listed-toggle')?.addEventListener('change', function() {
240 - var childId = this.dataset.childId;
241 - var listed = !this.checked;
242 - fetch('/api/items/' + bundleId + '/bundle/' + childId + '/listed', {
243 - method: 'PUT',
244 - headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
245 - body: JSON.stringify({ listed: listed })
246 - })
247 - .catch(function(err) { showToast(err.message); });
248 - });
249 - }
250 -
251 - document.querySelectorAll('.bundle-row').forEach(attachRowHandlers);
252 -
253 - function updateCount(delta) {
254 - var el = document.getElementById('bundle-count');
255 - el.textContent = parseInt(el.textContent) + delta;
256 - }
257 -
258 - function escapeHtml(s) {
259 - var d = document.createElement('div');
260 - d.textContent = s;
261 - return d.innerHTML;
262 - }
263 - })();
264 - </script>
265 171 {% endif %}
266 172
267 173 <!-- Sections Management -->
@@ -323,195 +229,7 @@
323 229 </div>
324 230 </div>
325 231
326 - <script>
327 - (function() {
328 - var itemId = '{{ item.id }}';
329 -
330 - document.getElementById('add-sec-btn').addEventListener('click', function() {
331 - var title = document.getElementById('new-sec-title').value.trim();
332 - var body = document.getElementById('new-sec-body').value;
333 - var status = document.getElementById('sec-add-status');
334 - if (!title) { status.textContent = 'Title is required'; return; }
335 - this.disabled = true;
336 - status.textContent = '';
337 -
338 - fetch('/api/items/' + itemId + '/sections', {
339 - method: 'POST',
340 - headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
341 - body: JSON.stringify({ title: title, body: body })
342 - })
343 - .then(function(res) {
344 - if (!res.ok) return res.json().then(function(d) { throw new Error(d.error || 'Failed'); });
345 - return res.json();
346 - })
347 - .then(function(sec) {
348 - var empty = document.getElementById('sections-empty');
349 - if (empty) empty.remove();
350 - var row = document.createElement('div');
351 - row.className = 'section-mgmt-row';
352 - row.dataset.id = sec.id;
353 - row.style.cssText = 'display:flex;align-items:center;gap:0.75rem;padding:0.6rem 0;border-bottom:1px solid var(--border);';
354 - row.innerHTML =
355 - '<span style="flex:1;font-weight:bold;">' + escapeHtml(sec.title) + '</span>' +
356 - '<span style="font-size:0.8rem;opacity:0.6;">' + (sec.body || '').length + ' chars</span>' +
357 - '<button type="button" class="secondary section-edit-btn" data-id="' + sec.id + '" style="padding:0.25rem 0.6rem;font-size:0.8rem;">Edit</button>' +
358 - '<button type="button" class="secondary section-del-btn" data-id="' + sec.id + '" style="padding:0.25rem 0.6rem;font-size:0.8rem;">Delete</button>';
359 - document.getElementById('sections-list').appendChild(row);
360 - attachSectionHandlers(row);
361 - updateSectionCount(1);
362 - document.getElementById('new-sec-title').value = '';
363 - document.getElementById('new-sec-body').value = '';
364 - document.getElementById('section-add-details').removeAttribute('open');
365 - })
366 - .catch(function(err) { status.textContent = err.message; })
367 - .finally(function() { document.getElementById('add-sec-btn').disabled = false; });
368 - });
369 232
370 - function attachSectionHandlers(row) {
371 - row.querySelector('.section-del-btn').addEventListener('click', function() {
372 - var id = this.dataset.id;
373 - if (!confirm('Delete this section?')) return;
374 - fetch('/api/sections/' + id, { method: 'DELETE', headers: csrfHeaders() })
375 - .then(function(res) { if (res.ok) { row.remove(); updateSectionCount(-1); } else showToast('Failed to delete'); })
376 - .catch(function() { showToast('Failed to delete'); });
377 - });
378 233
379 - row.querySelector('.section-edit-btn').addEventListener('click', function() {
380 - var id = this.dataset.id;
381 - var modal = document.getElementById('section-edit-modal');
382 - document.getElementById('edit-sec-id').value = id;
383 - document.getElementById('edit-sec-title').value = row.querySelector('span[style*="font-weight"]').textContent;
384 - document.getElementById('edit-sec-body').value = '';
385 - document.getElementById('sec-edit-status').textContent = 'Loading...';
386 - modal.style.display = 'block';
387 234
388 - fetch('/api/items/' + itemId + '/sections')
389 - .then(function(r) { return r.json(); })
390 - .then(function(resp) {
391 - var sec = resp.data.find(function(s) { return s.id === id; });
392 - if (sec) {
393 - document.getElementById('edit-sec-title').value = sec.title;
394 - document.getElementById('edit-sec-body').value = sec.body;
395 - }
396 - document.getElementById('sec-edit-status').textContent = '';
397 - })
398 - .catch(function() { document.getElementById('sec-edit-status').textContent = 'Failed to load'; });
399 - });
400 - }
401 -
402 - document.getElementById('save-sec-btn').addEventListener('click', function() {
403 - var id = document.getElementById('edit-sec-id').value;
404 - var title = document.getElementById('edit-sec-title').value.trim();
405 - var body = document.getElementById('edit-sec-body').value;
406 - var status = document.getElementById('sec-edit-status');
407 - if (!title) { status.textContent = 'Title is required'; return; }
408 - this.disabled = true;
409 -
410 - fetch('/api/sections/' + id, {
411 - method: 'PUT',
412 - headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
413 - body: JSON.stringify({ title: title, body: body })
414 - })
415 - .then(function(res) {
416 - if (!res.ok) return res.json().then(function(d) { throw new Error(d.error || 'Failed'); });
417 - return res.json();
418 - })
419 - .then(function(sec) {
420 - var row = document.querySelector('.section-mgmt-row[data-id="' + id + '"]');
421 - if (row) {
422 - row.querySelector('span[style*="font-weight"]').textContent = sec.title;
423 - row.querySelector('span[style*="opacity"]').textContent = (sec.body || '').length + ' chars';
424 - }
425 - document.getElementById('section-edit-modal').style.display = 'none';
426 - })
427 - .catch(function(err) { status.textContent = err.message; })
428 - .finally(function() { document.getElementById('save-sec-btn').disabled = false; });
429 - });
430 -
431 - document.getElementById('cancel-sec-btn').addEventListener('click', function() {
432 - document.getElementById('section-edit-modal').style.display = 'none';
433 - });
434 -
435 - document.querySelectorAll('.section-mgmt-row').forEach(attachSectionHandlers);
436 -
437 - function updateSectionCount(delta) {
438 - var el = document.getElementById('section-count');
439 - el.textContent = parseInt(el.textContent) + delta;
440 - }
441 -
442 - function escapeHtml(s) {
443 - var d = document.createElement('div');
444 - d.textContent = s;
445 - return d.innerHTML;
446 - }
447 - })();
448 - </script>
449 -
450 - <!-- AI Tier Disclosure Toggle -->
451 - <script>
452 - (function() {
453 - var tierSelect = document.getElementById('ai_tier');
454 - var disclosureRow = document.getElementById('ai-disclosure-row');
455 - if (tierSelect && disclosureRow) {
456 - tierSelect.addEventListener('change', function() {
457 - disclosureRow.style.display = this.value === 'assisted' ? '' : 'none';
458 - });
459 - }
460 - })();
461 - </script>
462 -
463 - <!-- Tag Search Script -->
464 - <script>
465 - var tagSearchTimeout;
466 - function searchTags(q) {
467 - clearTimeout(tagSearchTimeout);
468 - var suggestions = document.getElementById('tag-suggestions');
469 - if (!q.trim()) { suggestions.style.display = 'none'; return; }
470 - tagSearchTimeout = setTimeout(function() {
471 - fetch('/api/tags/search?q=' + encodeURIComponent(q))
472 - .then(function(r) { return r.json(); })
473 - .then(function(tags) {
474 - if (!tags.length) { suggestions.style.display = 'none'; return; }
475 - suggestions.innerHTML = '';
476 - tags.forEach(function(t) {
477 - var div = document.createElement('div');
478 - div.style.cssText = 'padding: 0.4rem 0.6rem; cursor: pointer; font-size: 0.85rem;';
479 - div.textContent = t.name;
480 - div.addEventListener('mouseover', function() { this.style.background = 'var(--border)'; });
481 - div.addEventListener('mouseout', function() { this.style.background = 'transparent'; });
482 - div.addEventListener('click', function() { addTagById(t.id); });
483 - suggestions.appendChild(div);
484 - });
485 - suggestions.style.display = 'block';
486 - })
487 - .catch(function() {
488 - suggestions.style.display = 'none';
489 - });
490 - }, 200);
491 - }
492 -
493 - function addTagById(tagId) {
494 - var form = new FormData();
495 - form.append('tag_id', tagId);
496 - fetch('/api/items/{{ item.id }}/tags', { method: 'POST', headers: csrfHeaders(), body: form })
497 - .then(function(r) { return r.text(); })
498 - .then(function(html) {
499 - if (html) {
500 - document.getElementById('tags-container').insertAdjacentHTML('beforeend', html);
501 - }
502 - document.getElementById('item-tags').value = '';
503 - document.getElementById('tag-suggestions').style.display = 'none';
504 - htmx.process(document.getElementById('tags-container'));
505 - })
506 - .catch(function() {
507 - var msg = document.getElementById('item-save-status');
508 - if (msg) msg.textContent = 'Failed to add tag. Please try again.';
509 - });
510 - }
511 -
512 - document.addEventListener('click', function(e) {
513 - if (!e.target.closest('#item-tags') && !e.target.closest('#tag-suggestions')) {
514 - document.getElementById('tag-suggestions').style.display = 'none';
515 - }
516 - });
517 - </script>
235 + </div>