Skip to main content

max / makenotwork

11.8 KB · 287 lines History Blame Raw
1 <div class="content-section">
2 <div class="section-header">
3 <h2 class="subsection-title">Cloud Sync</h2>
4 </div>
5
6 <p class="form-hint mb-3">Cloud Sync lets your desktop or mobile apps sync data with Makenotwork using end-to-end encryption. Each app gets an API key your app uses to connect. <a href="/docs/developer/synckit">Learn more</a></p>
7 <p class="form-hint mb-5">Most creators don't need this: it's for developers who ship their own software through Makenotwork.</p>
8
9 <!-- Create new app form -->
10 <details class="form-section user-synckit-create">
11 <summary><h2 class="subsection-title">Create New App</h2></summary>
12 <div class="user-synckit-create-body">
13
14 <form id="synckit-create-form" class="user-synckit-create-form">
15 <div>
16 <label for="synckit-app-name">App Name</label>
17 <input type="text" id="synckit-app-name" name="name" required
18 placeholder="e.g. GoingsOn Desktop">
19 </div>
20 <div>
21 <label for="synckit-app-project">Project (optional)</label>
22 <select id="synckit-app-project">
23 <option value="">None</option>
24 {% for project in projects %}
25 <option value="{{ project.id }}">{{ project.title }}</option>
26 {% endfor %}
27 </select>
28 </div>
29 <button type="submit" class="btn">Create</button>
30 </form>
31 <div id="synckit-create-status"></div>
32 </div>
33 </details>
34
35 {% if apps.is_empty() %}
36 <p class="muted">No sync apps yet. Create one above to get started.</p>
37 {% else %}
38 <div class="scroll-x">
39 <table class="data-table">
40 <thead>
41 <tr>
42 <th>Name</th>
43 <th title="Identifier used for over-the-air updates">Update Slug</th>
44 <th>Linked To</th>
45 <th title="Secret key your app uses to authenticate with Cloud Sync">API Key</th>
46 <th>Status</th>
47 <th title="Number of devices currently syncing">Devices</th>
48 <th title="Recent sync activity">Log Entries</th>
49 <th>Created</th>
50 <th>Billing</th>
51 <th>Actions</th>
52 </tr>
53 </thead>
54 <tbody>
55 {% for app in apps %}
56 <tr>
57 <td>{{ app.name }}</td>
58 <td id="synckit-slug-cell-{{ app.id }}">
59 {% if let Some(s) = app.slug %}
60 <code class="user-synckit-code">{{ s }}</code>
61 {% else %}
62 <span class="dimmed">-</span>
63 {% endif %}
64 <button class="btn-small btn-secondary user-synckit-inline-btn"
65 onclick="syncKitShowSlugForm('{{ app.id }}', '{{ app.slug.as_deref().unwrap_or_default() }}')">Set</button>
66 </td>
67 <td id="synckit-link-cell-{{ app.id }}">
68 {% if let Some(pname) = app.project_name %}
69 {% if let Some(pslug) = app.project_slug %}
70 <a href="/dashboard/project/{{ pslug }}">{{ pname }}</a>
71 {% endif %}
72 {% if let Some(ititle) = app.item_title %}
73 <span class="text-sm dimmed"> / {{ ititle }}</span>
74 {% endif %}
75 {% else %}
76 <span class="dimmed">-</span>
77 {% endif %}
78 <button class="btn-small btn-secondary user-synckit-inline-btn"
79 onclick="syncKitShowLinkForm('{{ app.id }}')">Link</button>
80 </td>
81 <td>
82 <code id="synckit-key-display-{{ app.id }}" class="user-synckit-code">{{ app.api_key_masked }}</code>
83 <span class="form-hint user-synckit-hint">(use Regenerate to get a new key)</span>
84 </td>
85 <td>
86 {% if app.is_active %}
87 <span class="user-synckit-status-active">Active</span>
88 {% else %}
89 <span class="user-synckit-status-inactive">Inactive</span>
90 {% endif %}
91 </td>
92 <td>{{ app.device_count }}</td>
93 <td>{{ app.log_entry_count }}</td>
94 <td>{{ app.created_at }}</td>
95 <td>{% include "partials/synckit_billing_panel.html" %}</td>
96 <td class="nowrap">
97 <button class="btn-small btn-secondary user-synckit-row-btn"
98 onclick="syncKitRegenKey('{{ app.id }}')">Regenerate Key</button>
99 <button class="btn-small btn-danger user-synckit-row-btn"
100 onclick="syncKitDeleteApp('{{ app.id }}', '{{ app.name }}')">Delete</button>
101 </td>
102 </tr>
103 {% endfor %}
104 </tbody>
105 </table>
106 </div>
107 {% endif %}
108 </div>
109
110 <script>
111 if (window.initSyncKitBilling) window.initSyncKitBilling();
112 function syncKitCopyKey(event, key) {
113 navigator.clipboard.writeText(key).then(function() {
114 var btn = event.target;
115 var orig = btn.textContent;
116 btn.textContent = 'Copied';
117 setTimeout(function() { btn.textContent = orig; }, 1500);
118 });
119 }
120
121 function syncKitRegenKey(appId) {
122 if (!confirm('Regenerate API key? All existing clients using the current key will stop working.')) return;
123 fetch('/api/sync/apps/' + appId + '/regenerate-key', {
124 method: 'POST',
125 credentials: 'same-origin',
126 headers: csrfHeaders()
127 }).then(function(res) {
128 if (res.ok) {
129 document.getElementById('tab-synckit').click();
130 } else {
131 showToast('Failed to regenerate key.');
132 }
133 }).catch(function() {
134 showToast('Network error. Please check your connection and try again.');
135 });
136 }
137
138 function syncKitDeleteApp(appId, name) {
139 if (!confirm('Delete sync app "' + name + '"? This will remove all devices and sync data for this app.')) return;
140 fetch('/api/sync/apps/' + appId, {
141 method: 'DELETE',
142 credentials: 'same-origin',
143 headers: csrfHeaders()
144 }).then(function(res) {
145 if (res.ok) {
146 document.getElementById('tab-synckit').click();
147 } else {
148 showToast('Failed to delete app.');
149 }
150 }).catch(function() {
151 showToast('Network error. Please check your connection and try again.');
152 });
153 }
154
155 function syncKitShowSlugForm(appId, currentSlug) {
156 var cell = document.getElementById('synckit-slug-cell-' + appId);
157 if (!cell) return;
158 cell.innerHTML = '';
159 var input = document.createElement('input');
160 input.type = 'text';
161 input.value = currentSlug;
162 input.placeholder = 'e.g. goingson';
163 input.className = 'user-synckit-edit-input input--sm w-120';
164 cell.appendChild(input);
165 cell.appendChild(document.createTextNode(' '));
166 var saveBtn = document.createElement('button');
167 saveBtn.className = 'btn-small user-synckit-edit-btn';
168 saveBtn.textContent = 'Save';
169 saveBtn.addEventListener('click', function() {
170 var slug = input.value.trim();
171 if (!slug) return;
172 fetch('/api/sync/apps/' + appId + '/slug', {
173 method: 'PUT',
174 credentials: 'same-origin',
175 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
176 body: JSON.stringify({ slug: slug })
177 }).then(function(res) {
178 if (res.ok) {
179 document.getElementById('tab-synckit').click();
180 showToast('Update slug set to "' + slug + '".');
181 } else {
182 res.text().then(function(t) { showToast(t || 'Failed to set slug.'); });
183 }
184 }).catch(function() {
185 showToast('Network error. Please check your connection and try again.');
186 });
187 });
188 cell.appendChild(saveBtn);
189 cell.appendChild(document.createTextNode(' '));
190 var cancelBtn = document.createElement('button');
191 cancelBtn.className = 'btn-small secondary user-synckit-edit-btn';
192 cancelBtn.textContent = 'Cancel';
193 cancelBtn.addEventListener('click', function() { document.getElementById('tab-synckit').click(); });
194 cell.appendChild(cancelBtn);
195 }
196
197 function syncKitShowLinkForm(appId) {
198 var cell = document.getElementById('synckit-link-cell-' + appId);
199 if (!cell) return;
200 cell.innerHTML = '';
201 var select = document.createElement('select');
202 select.id = 'synckit-link-select-' + appId;
203 select.className = 'user-synckit-edit-select input--sm';
204 var noneOpt = document.createElement('option');
205 noneOpt.value = '';
206 noneOpt.textContent = 'None';
207 select.appendChild(noneOpt);
208 {% for project in projects %}
209 var opt{{ loop.index }} = document.createElement('option');
210 opt{{ loop.index }}.value = '{{ project.id }}';
211 opt{{ loop.index }}.textContent = '{{ project.title }}';
212 select.appendChild(opt{{ loop.index }});
213 {% endfor %}
214 cell.appendChild(select);
215 cell.appendChild(document.createTextNode(' '));
216 var saveBtn = document.createElement('button');
217 saveBtn.className = 'btn-small user-synckit-edit-btn';
218 saveBtn.textContent = 'Save';
219 saveBtn.addEventListener('click', function() { syncKitSaveLink(appId); });
220 cell.appendChild(saveBtn);
221 cell.appendChild(document.createTextNode(' '));
222 var cancelBtn = document.createElement('button');
223 cancelBtn.className = 'btn-small secondary user-synckit-edit-btn';
224 cancelBtn.textContent = 'Cancel';
225 cancelBtn.addEventListener('click', function() { document.getElementById('tab-synckit').click(); });
226 cell.appendChild(cancelBtn);
227 }
228
229 function syncKitSaveLink(appId) {
230 var sel = document.getElementById('synckit-link-select-' + appId);
231 if (!sel) return;
232 var projectId = sel.value;
233 fetch('/api/sync/apps/' + appId + '/link', {
234 method: 'PUT',
235 credentials: 'same-origin',
236 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
237 body: JSON.stringify({ project_id: projectId || null, item_id: null })
238 }).then(function(res) {
239 if (res.ok) {
240 document.getElementById('tab-synckit').click();
241 } else {
242 showToast('Failed to update link.');
243 }
244 }).catch(function() {
245 showToast('Network error. Please check your connection and try again.');
246 });
247 }
248
249 (function() {
250 var form = document.getElementById('synckit-create-form');
251 if (!form) return;
252 form.addEventListener('submit', function(e) {
253 e.preventDefault();
254 var nameInput = document.getElementById('synckit-app-name');
255 var name = nameInput.value.trim();
256 if (!name) return;
257
258 var projectSelect = document.getElementById('synckit-app-project');
259 var projectId = projectSelect ? projectSelect.value : '';
260
261 var body = { name: name };
262 if (projectId) body.project_id = projectId;
263
264 fetch('/api/sync/apps', {
265 method: 'POST',
266 credentials: 'same-origin',
267 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
268 body: JSON.stringify(body)
269 }).then(function(res) {
270 if (res.ok) {
271 document.getElementById('tab-synckit').click();
272 } else {
273 return res.text().then(function(t) {
274 var statusEl = document.getElementById('synckit-create-status');
275 statusEl.className = 'user-synckit-create-error';
276 statusEl.textContent = t || 'Failed to create app.';
277 });
278 }
279 }).catch(function() {
280 var statusEl = document.getElementById('synckit-create-status');
281 statusEl.className = 'user-synckit-create-error';
282 statusEl.textContent = 'Network error. Please check your connection and try again.';
283 });
284 });
285 })();
286 </script>
287