Skip to main content

max / makenotwork

10.4 KB · 224 lines History Blame Raw
1 <div class="tab-docs"><a href="/docs/developer/synckit">Docs: Cloud Sync &rarr;</a></div>
2
3 <div class="content-section">
4 <div class="section-header">
5 <h2 class="subsection-title">Cloud Sync</h2>
6 </div>
7
8 <p class="form-hint proj-synckit-intro">Cloud Sync lets your desktop or mobile apps sync data with Makenotwork using end-to-end encryption. If you're building an app that stores user data, create a sync app here to get an API key your app uses to connect. <a href="/docs/developer/synckit">Learn more</a></p>
9 <p class="form-hint proj-synckit-intro-secondary">Most creators don't need this: it's for developers who ship their own software through Makenotwork.</p>
10
11 <!-- Create new app form -->
12 <details class="form-section proj-synckit-create">
13 <summary><h2 class="subsection-title">Create New App</h2></summary>
14 <div class="proj-synckit-create-body">
15
16 <form id="synckit-create-form" class="proj-synckit-create-form">
17 <div>
18 <label for="synckit-app-name" class="proj-synckit-label">App Name</label>
19 <input type="text" id="synckit-app-name" name="name" required
20 placeholder="e.g. GoingsOn Desktop"
21 class="proj-synckit-name-input input--sm w-220">
22 </div>
23 <button type="submit" class="btn">Create</button>
24 </form>
25 <div id="synckit-create-status"></div>
26 </div>
27 </details>
28
29 {% if apps.is_empty() %}
30 <p class="muted">No sync apps for this project yet. Create one above to get started.</p>
31 {% else %}
32 <div class="scroll-x">
33 <table class="data-table">
34 <thead>
35 <tr>
36 <th>Name</th>
37 <th title="Identifier used for over-the-air updates">Update Slug</th>
38 <th title="Secret key your app uses to authenticate with Cloud Sync">API Key</th>
39 <th>Status</th>
40 <th title="Number of devices currently syncing">Devices</th>
41 <th title="Recent sync activity">Log Entries</th>
42 <th>Created</th>
43 <th>Billing</th>
44 <th>Actions</th>
45 </tr>
46 </thead>
47 <tbody>
48 {% for app in apps %}
49 <tr>
50 <td>{{ app.name }}</td>
51 <td id="synckit-slug-cell-{{ app.id }}">
52 {% if let Some(s) = app.slug %}
53 <code class="proj-synckit-code">{{ s }}</code>
54 {% else %}
55 <span class="dimmed">-</span>
56 {% endif %}
57 <button class="btn-small btn-secondary proj-synckit-btn-tight"
58 onclick="syncKitShowSlugForm('{{ app.id }}', '{{ app.slug.as_deref().unwrap_or_default() }}')">Set</button>
59 </td>
60 <td>
61 <code id="synckit-key-display-{{ app.id }}" class="proj-synckit-code">{{ app.api_key_masked }}</code>
62 <span class="form-hint proj-synckit-hint-inline">(use Regenerate to get a new key)</span>
63 </td>
64 <td>
65 {% if app.is_active %}
66 <span class="proj-synckit-status-active">Active</span>
67 {% else %}
68 <span class="proj-synckit-status-inactive">Inactive</span>
69 {% endif %}
70 </td>
71 <td>{{ app.device_count }}</td>
72 <td>{{ app.log_entry_count }}</td>
73 <td>{{ app.created_at }}</td>
74 <td>{% include "partials/synckit_billing_panel.html" %}</td>
75 <td class="nowrap">
76 <button class="btn-small btn-secondary proj-synckit-btn-row"
77 onclick="syncKitRegenKey('{{ app.id }}')">Regenerate Key</button>
78 <button class="btn-small btn-danger proj-synckit-btn-row"
79 onclick="syncKitDeleteApp('{{ app.id }}', '{{ app.name }}')">Delete</button>
80 </td>
81 </tr>
82 {% endfor %}
83 </tbody>
84 </table>
85 </div>
86 {% endif %}
87 </div>
88
89 <script>
90 if (window.initSyncKitBilling) window.initSyncKitBilling();
91 function syncKitRegenKey(appId) {
92 if (!confirm('Regenerate API key? All existing clients using the current key will stop working.')) return;
93 fetch('/api/sync/apps/' + appId + '/regenerate-key', {
94 method: 'POST',
95 credentials: 'same-origin',
96 headers: csrfHeaders()
97 }).then(function(res) {
98 if (res.ok) {
99 return res.json().then(function(data) {
100 // Show the new key with a copy button
101 var display = document.getElementById('synckit-key-display-' + appId);
102 if (display && data.api_key) {
103 display.parentElement.innerHTML =
104 '<code class="proj-synckit-code--wrap">' + data.api_key + '</code>' +
105 ' <button class="btn-small btn-secondary proj-synckit-btn-row" ' +
106 'onclick="navigator.clipboard.writeText(\'' + data.api_key + '\').then(function(){this.textContent=\'Copied\';var b=this;setTimeout(function(){b.textContent=\'Copy\'},1500)}.bind(this))">Copy</button>' +
107 ' <span class="form-hint proj-synckit-hint-warn">Save this key now: it cannot be shown again.</span>';
108 }
109 showToast('Key regenerated. Copy it now: it will not be shown again.');
110 });
111 } else {
112 showToast('Failed to regenerate key.');
113 }
114 }).catch(function() {
115 showToast('Network error. Please check your connection and try again.');
116 });
117 }
118
119 function syncKitDeleteApp(appId, name) {
120 if (!confirm('Delete sync app "' + name + '"? This will remove all devices and sync data for this app.')) return;
121 fetch('/api/sync/apps/' + appId, {
122 method: 'DELETE',
123 credentials: 'same-origin',
124 headers: csrfHeaders()
125 }).then(function(res) {
126 if (res.ok) {
127 document.getElementById('tab-synckit').click();
128 } else {
129 showToast('Failed to delete app.');
130 }
131 }).catch(function() {
132 showToast('Network error. Please check your connection and try again.');
133 });
134 }
135
136 function syncKitShowSlugForm(appId, currentSlug) {
137 var cell = document.getElementById('synckit-slug-cell-' + appId);
138 if (!cell) return;
139 cell.innerHTML = '';
140 var input = document.createElement('input');
141 input.type = 'text';
142 input.value = currentSlug;
143 input.placeholder = 'e.g. goingson';
144 input.className = 'proj-synckit-slug-input input--sm w-120';
145 cell.appendChild(input);
146 cell.appendChild(document.createTextNode(' '));
147 var saveBtn = document.createElement('button');
148 saveBtn.className = 'btn-small proj-synckit-btn-row';
149 saveBtn.textContent = 'Save';
150 saveBtn.addEventListener('click', function() {
151 var slug = input.value.trim();
152 if (!slug) return;
153 fetch('/api/sync/apps/' + appId + '/slug', {
154 method: 'PUT',
155 credentials: 'same-origin',
156 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
157 body: JSON.stringify({ slug: slug })
158 }).then(function(res) {
159 if (res.ok) {
160 document.getElementById('tab-synckit').click();
161 showToast('Update slug set to "' + slug + '".');
162 } else {
163 res.text().then(function(t) { showToast(t || 'Failed to set slug.'); });
164 }
165 }).catch(function() {
166 showToast('Network error. Please check your connection and try again.');
167 });
168 });
169 cell.appendChild(saveBtn);
170 cell.appendChild(document.createTextNode(' '));
171 var cancelBtn = document.createElement('button');
172 cancelBtn.className = 'btn-small secondary proj-synckit-btn-row';
173 cancelBtn.textContent = 'Cancel';
174 cancelBtn.addEventListener('click', function() { document.getElementById('tab-synckit').click(); });
175 cell.appendChild(cancelBtn);
176 }
177
178 (function() {
179 var form = document.getElementById('synckit-create-form');
180 if (!form) return;
181 form.addEventListener('submit', function(e) {
182 e.preventDefault();
183 var nameInput = document.getElementById('synckit-app-name');
184 var name = nameInput.value.trim();
185 if (!name) return;
186
187 fetch('/api/sync/apps', {
188 method: 'POST',
189 credentials: 'same-origin',
190 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
191 body: JSON.stringify({ name: name, project_id: '{{ project_id }}' })
192 }).then(function(res) {
193 if (res.ok) {
194 return res.json().then(function(data) {
195 var statusEl = document.getElementById('synckit-create-status');
196 if (statusEl && data.api_key) {
197 statusEl.className = 'proj-synckit-create-status--reset';
198 statusEl.innerHTML =
199 '<div class="proj-synckit-new-key">' +
200 '<p class="proj-synckit-new-key-heading">Save this API key now: it cannot be shown again.</p>' +
201 '<code class="proj-synckit-code--selectable">' + data.api_key + '</code>' +
202 ' <button class="btn-small btn-secondary proj-synckit-btn-row" ' +
203 'onclick="navigator.clipboard.writeText(\'' + data.api_key + '\').then(function(){this.textContent=\'Copied\';var b=this;setTimeout(function(){b.textContent=\'Copy\'},1500)}.bind(this))">Copy</button>' +
204 '<button class="btn-small proj-synckit-btn-tight" ' +
205 'onclick="document.getElementById(\'tab-synckit\').click()">Done</button>' +
206 '</div>';
207 }
208 });
209 } else {
210 return res.text().then(function(t) {
211 var statusEl = document.getElementById('synckit-create-status');
212 statusEl.className = 'proj-synckit-create-status--error';
213 statusEl.textContent = t || 'Failed to create app.';
214 });
215 }
216 }).catch(function() {
217 var statusEl = document.getElementById('synckit-create-status');
218 statusEl.className = 'proj-synckit-create-status--error';
219 statusEl.textContent = 'Network error. Please check your connection and try again.';
220 });
221 });
222 })();
223 </script>
224