Skip to main content

max / makenotwork

11.8 KB · 257 lines History Blame Raw
1 /* Makenotwork — SyncKit dashboard tabs (project + user).
2 *
3 * Loaded once globally (base.html), like synckit-billing.js. Each tab partial
4 * calls window.initSyncKitTab(config) on render to wire its create form and
5 * pass tab-specific bits (edit-control classes, whether Regenerate shows the
6 * new key inline, the create-form mode). All table-row buttons call the global
7 * syncKit* functions below. No template value is ever interpolated into a JS
8 * string: ids/slugs/names arrive via data-* attributes, the project list via a
9 * <template> of <option>s — so titles/slugs can't break out of a string. */
10 'use strict';
11
12 (function () {
13 // Tab-specific config, refreshed on each initSyncKitTab() call (tabs are
14 // re-rendered HTMX partials; the script itself loads once).
15 var cfg = {
16 editInputClass: 'proj-synckit-slug-input input--sm w-120',
17 editBtnClass: 'proj-synckit-btn-row',
18 regenInline: false,
19 };
20
21 function netError() { showToast('Network error. Please check your connection and try again.'); }
22 function reloadTab() { document.getElementById('tab-synckit').click(); }
23
24 // A "Copy" button that copies `text` and briefly flips its label. Built via
25 // addEventListener so the secret key never lands in an inline onclick string.
26 function makeCopyButton(text, className) {
27 var btn = document.createElement('button');
28 btn.type = 'button';
29 btn.className = className;
30 btn.textContent = 'Copy';
31 btn.addEventListener('click', function () {
32 navigator.clipboard.writeText(text).then(function () {
33 btn.textContent = 'Copied';
34 setTimeout(function () { btn.textContent = 'Copy'; }, 1500);
35 });
36 });
37 return btn;
38 }
39
40 window.syncKitCopyKey = function (event, key) {
41 navigator.clipboard.writeText(key).then(function () {
42 var btn = event.target;
43 var orig = btn.textContent;
44 btn.textContent = 'Copied';
45 setTimeout(function () { btn.textContent = orig; }, 1500);
46 });
47 };
48
49 window.syncKitRegenKey = function (appId) {
50 if (!confirm('Regenerate API key? All existing clients using the current key will stop working.')) return;
51 fetch('/api/sync/apps/' + appId + '/regenerate-key', {
52 method: 'POST',
53 credentials: 'same-origin',
54 headers: csrfHeaders(),
55 }).then(function (res) {
56 if (!res.ok) { showToast('Failed to regenerate key.'); return; }
57 if (!cfg.regenInline) { reloadTab(); return; }
58 // Project tab: reveal the new key inline with a copy button.
59 return res.json().then(function (data) {
60 var display = document.getElementById('synckit-key-display-' + appId);
61 if (display && data.api_key) {
62 var parent = display.parentElement;
63 parent.innerHTML = '';
64 var code = document.createElement('code');
65 code.className = 'proj-synckit-code--wrap';
66 code.textContent = data.api_key;
67 parent.appendChild(code);
68 parent.appendChild(document.createTextNode(' '));
69 parent.appendChild(makeCopyButton(data.api_key, 'btn-small btn-secondary proj-synckit-btn-row'));
70 parent.appendChild(document.createTextNode(' '));
71 var warn = document.createElement('span');
72 warn.className = 'form-hint proj-synckit-hint-warn';
73 warn.textContent = 'Save this key now: it cannot be shown again.';
74 parent.appendChild(warn);
75 }
76 showToast('Key regenerated. Copy it now: it will not be shown again.');
77 });
78 }).catch(netError);
79 };
80
81 window.syncKitDeleteApp = function (appId, name) {
82 if (!confirm('Delete sync app "' + name + '"? This will remove all devices and sync data for this app.')) return;
83 fetch('/api/sync/apps/' + appId, {
84 method: 'DELETE',
85 credentials: 'same-origin',
86 headers: csrfHeaders(),
87 }).then(function (res) {
88 if (res.ok) { reloadTab(); } else { showToast('Failed to delete app.'); }
89 }).catch(netError);
90 };
91
92 window.syncKitShowSlugForm = function (appId, currentSlug) {
93 var cell = document.getElementById('synckit-slug-cell-' + appId);
94 if (!cell) return;
95 cell.innerHTML = '';
96 var input = document.createElement('input');
97 input.type = 'text';
98 input.value = currentSlug;
99 input.placeholder = 'e.g. goingson';
100 input.className = cfg.editInputClass;
101 cell.appendChild(input);
102 cell.appendChild(document.createTextNode(' '));
103 var saveBtn = document.createElement('button');
104 saveBtn.className = 'btn-small ' + cfg.editBtnClass;
105 saveBtn.textContent = 'Save';
106 saveBtn.addEventListener('click', function () {
107 var slug = input.value.trim();
108 if (!slug) return;
109 fetch('/api/sync/apps/' + appId + '/slug', {
110 method: 'PUT',
111 credentials: 'same-origin',
112 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
113 body: JSON.stringify({ slug: slug }),
114 }).then(function (res) {
115 if (res.ok) {
116 reloadTab();
117 showToast('Update slug set to "' + slug + '".');
118 } else {
119 res.text().then(function (t) { showToast(t || 'Failed to set slug.'); });
120 }
121 }).catch(netError);
122 });
123 cell.appendChild(saveBtn);
124 cell.appendChild(document.createTextNode(' '));
125 var cancelBtn = document.createElement('button');
126 cancelBtn.className = 'btn-small secondary ' + cfg.editBtnClass;
127 cancelBtn.textContent = 'Cancel';
128 cancelBtn.addEventListener('click', reloadTab);
129 cell.appendChild(cancelBtn);
130 };
131
132 window.syncKitShowLinkForm = function (appId) {
133 var cell = document.getElementById('synckit-link-cell-' + appId);
134 if (!cell) return;
135 cell.innerHTML = '';
136 var select = document.createElement('select');
137 select.id = 'synckit-link-select-' + appId;
138 select.className = 'user-synckit-edit-select input--sm';
139 var noneOpt = document.createElement('option');
140 noneOpt.value = '';
141 noneOpt.textContent = 'None';
142 select.appendChild(noneOpt);
143 // Options come from a server-rendered <template> (HTML-escaped), not from
144 // values interpolated into this script.
145 var tpl = document.getElementById('synckit-projects-options');
146 if (tpl && tpl.content) {
147 tpl.content.querySelectorAll('option').forEach(function (opt) {
148 select.appendChild(opt.cloneNode(true));
149 });
150 }
151 cell.appendChild(select);
152 cell.appendChild(document.createTextNode(' '));
153 var saveBtn = document.createElement('button');
154 saveBtn.className = 'btn-small user-synckit-edit-btn';
155 saveBtn.textContent = 'Save';
156 saveBtn.addEventListener('click', function () { window.syncKitSaveLink(appId); });
157 cell.appendChild(saveBtn);
158 cell.appendChild(document.createTextNode(' '));
159 var cancelBtn = document.createElement('button');
160 cancelBtn.className = 'btn-small secondary user-synckit-edit-btn';
161 cancelBtn.textContent = 'Cancel';
162 cancelBtn.addEventListener('click', reloadTab);
163 cell.appendChild(cancelBtn);
164 };
165
166 window.syncKitSaveLink = function (appId) {
167 var sel = document.getElementById('synckit-link-select-' + appId);
168 if (!sel) return;
169 var projectId = sel.value;
170 fetch('/api/sync/apps/' + appId + '/link', {
171 method: 'PUT',
172 credentials: 'same-origin',
173 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
174 body: JSON.stringify({ project_id: projectId || null, item_id: null }),
175 }).then(function (res) {
176 if (res.ok) { reloadTab(); } else { showToast('Failed to update link.'); }
177 }).catch(netError);
178 };
179
180 // Project tab: render the just-created app's key inline (one-time reveal).
181 function showCreatedKeyInline(statusEl, apiKey) {
182 statusEl.className = 'proj-synckit-create-status--reset';
183 statusEl.innerHTML = '';
184 var wrap = document.createElement('div');
185 wrap.className = 'proj-synckit-new-key';
186 var heading = document.createElement('p');
187 heading.className = 'proj-synckit-new-key-heading';
188 heading.textContent = 'Save this API key now: it cannot be shown again.';
189 wrap.appendChild(heading);
190 var code = document.createElement('code');
191 code.className = 'proj-synckit-code--selectable';
192 code.textContent = apiKey;
193 wrap.appendChild(code);
194 wrap.appendChild(document.createTextNode(' '));
195 wrap.appendChild(makeCopyButton(apiKey, 'btn-small btn-secondary proj-synckit-btn-row'));
196 var doneBtn = document.createElement('button');
197 doneBtn.type = 'button';
198 doneBtn.className = 'btn-small proj-synckit-btn-tight';
199 doneBtn.textContent = 'Done';
200 doneBtn.addEventListener('click', reloadTab);
201 wrap.appendChild(doneBtn);
202 statusEl.appendChild(wrap);
203 }
204
205 function wireCreateForm() {
206 var form = document.getElementById('synckit-create-form');
207 if (!form) return;
208 form.addEventListener('submit', function (e) {
209 e.preventDefault();
210 var nameInput = document.getElementById('synckit-app-name');
211 var name = nameInput.value.trim();
212 if (!name) return;
213
214 var body = { name: name };
215 if (cfg.create && cfg.create.mode === 'project') {
216 if (cfg.create.projectId) body.project_id = cfg.create.projectId;
217 } else {
218 var projectSelect = document.getElementById('synckit-app-project');
219 var projectId = projectSelect ? projectSelect.value : '';
220 if (projectId) body.project_id = projectId;
221 }
222
223 fetch('/api/sync/apps', {
224 method: 'POST',
225 credentials: 'same-origin',
226 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
227 body: JSON.stringify(body),
228 }).then(function (res) {
229 var statusEl = document.getElementById('synckit-create-status');
230 if (res.ok) {
231 // User tab just reloads; project tab reveals the key inline.
232 if (!cfg.create || cfg.create.mode !== 'project') { reloadTab(); return; }
233 return res.json().then(function (data) {
234 if (statusEl && data.api_key) showCreatedKeyInline(statusEl, data.api_key);
235 });
236 }
237 return res.text().then(function (t) {
238 if (!statusEl) return;
239 statusEl.className = (cfg.create && cfg.create.errorClass) || 'proj-synckit-create-status--error';
240 statusEl.textContent = t || 'Failed to create app.';
241 });
242 }).catch(function () {
243 var statusEl = document.getElementById('synckit-create-status');
244 if (!statusEl) return;
245 statusEl.className = (cfg.create && cfg.create.errorClass) || 'proj-synckit-create-status--error';
246 statusEl.textContent = 'Network error. Please check your connection and try again.';
247 });
248 });
249 }
250
251 window.initSyncKitTab = function (config) {
252 cfg = config || {};
253 if (window.initSyncKitBilling) window.initSyncKitBilling();
254 wireCreateForm();
255 };
256 })();
257