Spaces:
Sleeping
Sleeping
chore: remove Granim and telemetry UI from frontend; switch Leaflet CSS to prefetch; git rm static/telemetry.html
Browse files- static/index.html +4 -56
- static/js/modules/api-client.js +1 -56
- static/js/modules/auth-manager.js +0 -16
- static/js/tree-track-app.js +0 -72
- static/map.html +3 -41
- static/sw.js +1 -1
- static/telemetry.html +0 -178
- version.json +1 -1
static/index.html
CHANGED
@@ -9,13 +9,10 @@
|
|
9 |
<title>TreeTrack - Professional Field Research</title>
|
10 |
<link rel="preconnect" href="https://unpkg.com" crossorigin>
|
11 |
<link rel="dns-prefetch" href="//unpkg.com">
|
12 |
-
<link rel="
|
13 |
<link rel="icon" type="image/png" href="/static/image/icons8-tree-96.png">
|
14 |
<link rel="apple-touch-icon" href="/static/image/icons8-tree-96.png">
|
15 |
<link rel="stylesheet" href="/static/css/design-system.css">
|
16 |
-
<!-- Granim.js for gradient animations (deferred) -->
|
17 |
-
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/granim.min.js" defer></script>
|
18 |
-
<style>
|
19 |
|
20 |
:root {
|
21 |
/* Colors */
|
@@ -93,18 +90,7 @@
|
|
93 |
overflow: hidden;
|
94 |
}
|
95 |
|
96 |
-
/*
|
97 |
-
#header-canvas {
|
98 |
-
position: absolute;
|
99 |
-
top: 0;
|
100 |
-
left: 0;
|
101 |
-
width: 100vw;
|
102 |
-
height: 100%;
|
103 |
-
z-index: 1;
|
104 |
-
background: linear-gradient(135deg, #0f172a, #1e293b, #16a085);
|
105 |
-
}
|
106 |
-
|
107 |
-
/* Ensure header content is above canvas */
|
108 |
.tt-header-content {
|
109 |
position: relative;
|
110 |
z-index: 2;
|
@@ -920,7 +906,7 @@
|
|
920 |
// Force refresh if we detect cached version
|
921 |
(function() {
|
922 |
const currentVersion = '5.1.1';
|
923 |
-
const timestamp = '
|
924 |
const lastVersion = sessionStorage.getItem('treetrack_version');
|
925 |
const lastTimestamp = sessionStorage.getItem('treetrack_timestamp');
|
926 |
|
@@ -1165,7 +1151,7 @@
|
|
1165 |
</div>
|
1166 |
</div>
|
1167 |
|
1168 |
-
<script type="module" src="/static/js/tree-track-app.js?v=5.1.1&t=
|
1169 |
|
1170 |
<script>
|
1171 |
// Idle-time prefetch of map assets to speed up first navigation
|
@@ -1190,44 +1176,6 @@
|
|
1190 |
link.addEventListener('mouseenter', prefetch, {once: true});
|
1191 |
link.addEventListener('touchstart', prefetch, {once: true});
|
1192 |
})();
|
1193 |
-
// Initialize Granim background animation on page load
|
1194 |
-
document.addEventListener('DOMContentLoaded', function() {
|
1195 |
-
// Define initializer and defer to idle time for better performance
|
1196 |
-
function startHeaderGranim() {
|
1197 |
-
new Granim({
|
1198 |
-
element: '#header-canvas',
|
1199 |
-
direction: 'diagonal',
|
1200 |
-
isPausedWhenNotInView: false,
|
1201 |
-
image: {
|
1202 |
-
// Forest path image
|
1203 |
-
source: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=2560&auto=format&fit=crop&ixlib=rb-4.0.3',
|
1204 |
-
position: ['center', 'center'],
|
1205 |
-
stretchMode: ['stretch', 'stretch'],
|
1206 |
-
blendingMode: 'multiply'
|
1207 |
-
},
|
1208 |
-
states: {
|
1209 |
-
"default-state": {
|
1210 |
-
gradients: [
|
1211 |
-
['#0f172a', '#1e293b', '#16a085'],
|
1212 |
-
['#1a202c', '#2d3748', '#27ae60'],
|
1213 |
-
['#2d3748', '#4a5568', '#2ecc71'],
|
1214 |
-
['#1a365d', '#2c5282', '#138d75'],
|
1215 |
-
['#0f172a', '#2d3748', '#16a085']
|
1216 |
-
],
|
1217 |
-
transitionSpeed: 12000
|
1218 |
-
}
|
1219 |
-
}
|
1220 |
-
});
|
1221 |
-
}
|
1222 |
-
|
1223 |
-
if (window.requestIdleCallback) {
|
1224 |
-
requestIdleCallback(() => { startHeaderGranim(); });
|
1225 |
-
} else {
|
1226 |
-
setTimeout(() => { startHeaderGranim(); }, 0);
|
1227 |
-
}
|
1228 |
-
|
1229 |
-
console.log('Scheduled header Granim initialization');
|
1230 |
-
});
|
1231 |
</script>
|
1232 |
</body>
|
1233 |
</html>
|
|
|
9 |
<title>TreeTrack - Professional Field Research</title>
|
10 |
<link rel="preconnect" href="https://unpkg.com" crossorigin>
|
11 |
<link rel="dns-prefetch" href="//unpkg.com">
|
12 |
+
<link rel="prefetch" href="https://unpkg.com/[email protected]/dist/leaflet.css">
|
13 |
<link rel="icon" type="image/png" href="/static/image/icons8-tree-96.png">
|
14 |
<link rel="apple-touch-icon" href="/static/image/icons8-tree-96.png">
|
15 |
<link rel="stylesheet" href="/static/css/design-system.css">
|
|
|
|
|
|
|
16 |
|
17 |
:root {
|
18 |
/* Colors */
|
|
|
90 |
overflow: hidden;
|
91 |
}
|
92 |
|
93 |
+
/* Ensure header content is above background */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
.tt-header-content {
|
95 |
position: relative;
|
96 |
z-index: 2;
|
|
|
906 |
// Force refresh if we detect cached version
|
907 |
(function() {
|
908 |
const currentVersion = '5.1.1';
|
909 |
+
const timestamp = '1755115883'; // Cache-busting bump
|
910 |
const lastVersion = sessionStorage.getItem('treetrack_version');
|
911 |
const lastTimestamp = sessionStorage.getItem('treetrack_timestamp');
|
912 |
|
|
|
1151 |
</div>
|
1152 |
</div>
|
1153 |
|
1154 |
+
<script type="module" src="/static/js/tree-track-app.js?v=5.1.1&t=1755115883"></script>
|
1155 |
|
1156 |
<script>
|
1157 |
// Idle-time prefetch of map assets to speed up first navigation
|
|
|
1176 |
link.addEventListener('mouseenter', prefetch, {once: true});
|
1177 |
link.addEventListener('touchstart', prefetch, {once: true});
|
1178 |
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1179 |
</script>
|
1180 |
</body>
|
1181 |
</html>
|
static/js/modules/api-client.js
CHANGED
@@ -178,70 +178,15 @@ export class ApiClient {
|
|
178 |
});
|
179 |
|
180 |
if (!response.ok) {
|
181 |
-
|
182 |
-
await this.sendTelemetry({
|
183 |
-
event_type: 'upload',
|
184 |
-
status: 'error',
|
185 |
-
metadata: {
|
186 |
-
type,
|
187 |
-
category,
|
188 |
-
filename: file.name,
|
189 |
-
size: file.size,
|
190 |
-
content_type: file.type,
|
191 |
-
response_status: response.status,
|
192 |
-
response_text: errText
|
193 |
-
}
|
194 |
-
}).catch(() => {});
|
195 |
throw new Error('Upload failed');
|
196 |
}
|
197 |
resJson = await response.json();
|
198 |
-
await this.sendTelemetry({
|
199 |
-
event_type: 'upload',
|
200 |
-
status: 'success',
|
201 |
-
metadata: {
|
202 |
-
type,
|
203 |
-
category,
|
204 |
-
filename: file.name,
|
205 |
-
size: file.size,
|
206 |
-
content_type: file.type,
|
207 |
-
server_filename: resJson.filename,
|
208 |
-
bucket: resJson.bucket
|
209 |
-
}
|
210 |
-
}).catch(() => {});
|
211 |
return resJson;
|
212 |
} catch (e) {
|
213 |
// Network or other errors
|
214 |
-
if (!resJson) {
|
215 |
-
await this.sendTelemetry({
|
216 |
-
event_type: 'upload',
|
217 |
-
status: 'error',
|
218 |
-
metadata: {
|
219 |
-
type,
|
220 |
-
category,
|
221 |
-
filename: file?.name,
|
222 |
-
size: file?.size,
|
223 |
-
content_type: file?.type,
|
224 |
-
error: e.message
|
225 |
-
}
|
226 |
-
}).catch(() => {});
|
227 |
-
}
|
228 |
throw e;
|
229 |
}
|
230 |
}
|
231 |
|
232 |
-
async sendTelemetry(payload) {
|
233 |
-
try {
|
234 |
-
const response = await this.authenticatedFetch('/api/telemetry', {
|
235 |
-
method: 'POST',
|
236 |
-
headers: {
|
237 |
-
'Content-Type': 'application/json'
|
238 |
-
},
|
239 |
-
body: JSON.stringify(payload)
|
240 |
-
});
|
241 |
-
if (!response) return;
|
242 |
-
// Best-effort; ignore response body
|
243 |
-
} catch (_) {
|
244 |
-
// swallow telemetry errors
|
245 |
-
}
|
246 |
-
}
|
247 |
}
|
|
|
178 |
});
|
179 |
|
180 |
if (!response.ok) {
|
181 |
+
// Avoid noisy telemetry; just throw error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
182 |
throw new Error('Upload failed');
|
183 |
}
|
184 |
resJson = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
185 |
return resJson;
|
186 |
} catch (e) {
|
187 |
// Network or other errors
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
188 |
throw e;
|
189 |
}
|
190 |
}
|
191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
192 |
}
|
static/js/modules/auth-manager.js
CHANGED
@@ -38,22 +38,6 @@ export class AuthManager {
|
|
38 |
|
39 |
async logout() {
|
40 |
try {
|
41 |
-
// Telemetry: logout initiated (best-effort)
|
42 |
-
try {
|
43 |
-
await fetch('/api/telemetry', {
|
44 |
-
method: 'POST',
|
45 |
-
headers: {
|
46 |
-
'Content-Type': 'application/json',
|
47 |
-
'Authorization': `Bearer ${this.authToken}`
|
48 |
-
},
|
49 |
-
body: JSON.stringify({
|
50 |
-
event_type: 'auth',
|
51 |
-
status: 'logout_initiated',
|
52 |
-
metadata: { source: 'client' }
|
53 |
-
})
|
54 |
-
});
|
55 |
-
} catch (_) { /* ignore */ }
|
56 |
-
|
57 |
await fetch('/api/auth/logout', {
|
58 |
method: 'POST',
|
59 |
headers: {
|
|
|
38 |
|
39 |
async logout() {
|
40 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
await fetch('/api/auth/logout', {
|
42 |
method: 'POST',
|
43 |
headers: {
|
static/js/tree-track-app.js
CHANGED
@@ -38,12 +38,6 @@ export class TreeTrackApp {
|
|
38 |
// Setup event listeners
|
39 |
this.setupEventListeners();
|
40 |
|
41 |
-
// Log UI load telemetry
|
42 |
-
this.apiClient.sendTelemetry({
|
43 |
-
event_type: 'ui',
|
44 |
-
status: 'view',
|
45 |
-
metadata: { page: 'index', action: 'load' }
|
46 |
-
}).catch(() => {});
|
47 |
|
48 |
|
49 |
// Load initial data
|
@@ -52,12 +46,6 @@ export class TreeTrackApp {
|
|
52 |
} catch (error) {
|
53 |
console.error('Error initializing TreeTrack app:', error);
|
54 |
this.uiManager.showMessage('Error initializing application: ' + error.message, 'error');
|
55 |
-
// Telemetry for init error
|
56 |
-
this.apiClient?.sendTelemetry({
|
57 |
-
event_type: 'ui',
|
58 |
-
status: 'error',
|
59 |
-
metadata: { page: 'index', action: 'init', error: error?.message }
|
60 |
-
}).catch(() => {});
|
61 |
}
|
62 |
}
|
63 |
|
@@ -170,40 +158,16 @@ export class TreeTrackApp {
|
|
170 |
let result;
|
171 |
if (this.formManager.isInEditMode()) {
|
172 |
const id = this.formManager.getCurrentEditId();
|
173 |
-
// Telemetry: update start
|
174 |
-
this.apiClient.sendTelemetry({
|
175 |
-
event_type: 'tree_update',
|
176 |
-
status: 'start',
|
177 |
-
metadata: { tree_id: id }
|
178 |
-
}).catch(() => {});
|
179 |
result = await this.apiClient.updateTree(id, treeData);
|
180 |
this.uiManager.showMessage(`Tree #${result.id} updated successfully!`, 'success');
|
181 |
-
// Telemetry: update success
|
182 |
-
this.apiClient.sendTelemetry({
|
183 |
-
event_type: 'tree_update',
|
184 |
-
status: 'success',
|
185 |
-
metadata: { tree_id: result.id }
|
186 |
-
}).catch(() => {});
|
187 |
// Exit edit mode silently after successful update
|
188 |
this.handleCancelEdit(false);
|
189 |
} else {
|
190 |
-
// Telemetry: create start
|
191 |
-
this.apiClient.sendTelemetry({
|
192 |
-
event_type: 'tree_create',
|
193 |
-
status: 'start',
|
194 |
-
metadata: { has_location_name: !!treeData.location_name }
|
195 |
-
}).catch(() => {});
|
196 |
result = await this.apiClient.saveTree(treeData);
|
197 |
this.uiManager.showMessage(
|
198 |
`Tree successfully added! Tree ID: ${result.id}. The form has been cleared for your next entry.`,
|
199 |
'success'
|
200 |
);
|
201 |
-
// Telemetry: create success
|
202 |
-
this.apiClient.sendTelemetry({
|
203 |
-
event_type: 'tree_create',
|
204 |
-
status: 'success',
|
205 |
-
metadata: { tree_id: result.id }
|
206 |
-
}).catch(() => {});
|
207 |
this.formManager.resetForm(true);
|
208 |
}
|
209 |
|
@@ -215,12 +179,6 @@ export class TreeTrackApp {
|
|
215 |
console.error('Error submitting form:', error);
|
216 |
this.uiManager.showMessage('Error saving tree: ' + error.message, 'error');
|
217 |
this.uiManager.focusFirstError();
|
218 |
-
// Telemetry: save/update error
|
219 |
-
this.apiClient.sendTelemetry({
|
220 |
-
event_type: this.formManager.isInEditMode() ? 'tree_update' : 'tree_create',
|
221 |
-
status: 'error',
|
222 |
-
metadata: { error: error?.message }
|
223 |
-
}).catch(() => {});
|
224 |
}
|
225 |
}
|
226 |
|
@@ -254,30 +212,12 @@ export class TreeTrackApp {
|
|
254 |
}
|
255 |
|
256 |
try {
|
257 |
-
// Telemetry: delete start
|
258 |
-
this.apiClient.sendTelemetry({
|
259 |
-
event_type: 'tree_delete',
|
260 |
-
status: 'start',
|
261 |
-
metadata: { tree_id: treeId }
|
262 |
-
}).catch(() => {});
|
263 |
await this.apiClient.deleteTree(treeId);
|
264 |
this.uiManager.showMessage(`Tree #${treeId} deleted successfully.`, 'success');
|
265 |
-
// Telemetry: delete success
|
266 |
-
this.apiClient.sendTelemetry({
|
267 |
-
event_type: 'tree_delete',
|
268 |
-
status: 'success',
|
269 |
-
metadata: { tree_id: treeId }
|
270 |
-
}).catch(() => {});
|
271 |
await this.loadTrees();
|
272 |
} catch (error) {
|
273 |
console.error('Error deleting tree:', error);
|
274 |
this.uiManager.showMessage('Error deleting tree: ' + error.message, 'error');
|
275 |
-
// Telemetry: delete error
|
276 |
-
this.apiClient.sendTelemetry({
|
277 |
-
event_type: 'tree_delete',
|
278 |
-
status: 'error',
|
279 |
-
metadata: { tree_id: treeId, error: error?.message }
|
280 |
-
}).catch(() => {});
|
281 |
}
|
282 |
}
|
283 |
|
@@ -299,21 +239,9 @@ export class TreeTrackApp {
|
|
299 |
trees = (trees || []).sort((a,b) => new Date(b.created_at) - new Date(a.created_at));
|
300 |
} catch (_) {}
|
301 |
this.uiManager.renderTreeList(trees);
|
302 |
-
// Telemetry: list loaded
|
303 |
-
this.apiClient.sendTelemetry({
|
304 |
-
event_type: 'tree_list',
|
305 |
-
status: 'success',
|
306 |
-
metadata: { count: Array.isArray(trees) ? trees.length : null }
|
307 |
-
}).catch(() => {});
|
308 |
} catch (error) {
|
309 |
console.error('Error loading trees:', error);
|
310 |
this.uiManager.showErrorState('treeList', 'Error loading trees');
|
311 |
-
// Telemetry: list error
|
312 |
-
this.apiClient.sendTelemetry({
|
313 |
-
event_type: 'tree_list',
|
314 |
-
status: 'error',
|
315 |
-
metadata: { error: error?.message }
|
316 |
-
}).catch(() => {});
|
317 |
}
|
318 |
}
|
319 |
|
|
|
38 |
// Setup event listeners
|
39 |
this.setupEventListeners();
|
40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
|
42 |
|
43 |
// Load initial data
|
|
|
46 |
} catch (error) {
|
47 |
console.error('Error initializing TreeTrack app:', error);
|
48 |
this.uiManager.showMessage('Error initializing application: ' + error.message, 'error');
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
}
|
50 |
}
|
51 |
|
|
|
158 |
let result;
|
159 |
if (this.formManager.isInEditMode()) {
|
160 |
const id = this.formManager.getCurrentEditId();
|
|
|
|
|
|
|
|
|
|
|
|
|
161 |
result = await this.apiClient.updateTree(id, treeData);
|
162 |
this.uiManager.showMessage(`Tree #${result.id} updated successfully!`, 'success');
|
|
|
|
|
|
|
|
|
|
|
|
|
163 |
// Exit edit mode silently after successful update
|
164 |
this.handleCancelEdit(false);
|
165 |
} else {
|
|
|
|
|
|
|
|
|
|
|
|
|
166 |
result = await this.apiClient.saveTree(treeData);
|
167 |
this.uiManager.showMessage(
|
168 |
`Tree successfully added! Tree ID: ${result.id}. The form has been cleared for your next entry.`,
|
169 |
'success'
|
170 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
this.formManager.resetForm(true);
|
172 |
}
|
173 |
|
|
|
179 |
console.error('Error submitting form:', error);
|
180 |
this.uiManager.showMessage('Error saving tree: ' + error.message, 'error');
|
181 |
this.uiManager.focusFirstError();
|
|
|
|
|
|
|
|
|
|
|
|
|
182 |
}
|
183 |
}
|
184 |
|
|
|
212 |
}
|
213 |
|
214 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
215 |
await this.apiClient.deleteTree(treeId);
|
216 |
this.uiManager.showMessage(`Tree #${treeId} deleted successfully.`, 'success');
|
|
|
|
|
|
|
|
|
|
|
|
|
217 |
await this.loadTrees();
|
218 |
} catch (error) {
|
219 |
console.error('Error deleting tree:', error);
|
220 |
this.uiManager.showMessage('Error deleting tree: ' + error.message, 'error');
|
|
|
|
|
|
|
|
|
|
|
|
|
221 |
}
|
222 |
}
|
223 |
|
|
|
239 |
trees = (trees || []).sort((a,b) => new Date(b.created_at) - new Date(a.created_at));
|
240 |
} catch (_) {}
|
241 |
this.uiManager.renderTreeList(trees);
|
|
|
|
|
|
|
|
|
|
|
|
|
242 |
} catch (error) {
|
243 |
console.error('Error loading trees:', error);
|
244 |
this.uiManager.showErrorState('treeList', 'Error loading trees');
|
|
|
|
|
|
|
|
|
|
|
|
|
245 |
}
|
246 |
}
|
247 |
|
static/map.html
CHANGED
@@ -13,9 +13,6 @@
|
|
13 |
<!-- Leaflet CSS -->
|
14 |
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
|
15 |
<link rel="stylesheet" href="/static/css/design-system.css">
|
16 |
-
<!-- Granim.js for gradient animations -->
|
17 |
-
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/granim.min.js"></script>
|
18 |
-
|
19 |
<style>
|
20 |
|
21 |
:root {
|
@@ -102,18 +99,7 @@
|
|
102 |
overflow: hidden;
|
103 |
}
|
104 |
|
105 |
-
/*
|
106 |
-
#map-header-canvas {
|
107 |
-
position: absolute;
|
108 |
-
top: 0;
|
109 |
-
left: 0;
|
110 |
-
width: 100vw;
|
111 |
-
height: 100%;
|
112 |
-
z-index: 1;
|
113 |
-
background: linear-gradient(135deg, #f8fafc, #e2e8f0, #cbd5e1);
|
114 |
-
}
|
115 |
-
|
116 |
-
/* Ensure header content is above canvas */
|
117 |
.tt-header-content {
|
118 |
position: relative;
|
119 |
z-index: 2;
|
@@ -813,7 +799,7 @@
|
|
813 |
// Force refresh if we detect cached version
|
814 |
(function() {
|
815 |
const currentVersion = '5.1.1';
|
816 |
-
const timestamp = '
|
817 |
const lastVersion = sessionStorage.getItem('treetrack_version');
|
818 |
const lastTimestamp = sessionStorage.getItem('treetrack_timestamp');
|
819 |
|
@@ -939,7 +925,7 @@ const timestamp = '1755114945'; // Current timestamp for cache busting
|
|
939 |
|
940 |
<!-- Leaflet JS -->
|
941 |
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
|
942 |
-
<script src="/static/map.js?v=5.1.1&t=
|
943 |
|
944 |
"default-state": {
|
945 |
gradients: [
|
@@ -954,7 +940,6 @@ const timestamp = '1755114945'; // Current timestamp for cache busting
|
|
954 |
}
|
955 |
});
|
956 |
|
957 |
-
console.log('Map header Granim with forest road background initialized synchronously');
|
958 |
// Defer any heavy data fetch; ensure map shell is visible first
|
959 |
if (window.requestAnimationFrame) {
|
960 |
requestAnimationFrame(() => setTimeout(() => {
|
@@ -963,28 +948,5 @@ const timestamp = '1755114945'; // Current timestamp for cache busting
|
|
963 |
}
|
964 |
});
|
965 |
</script>
|
966 |
-
<script>
|
967 |
-
// Emit telemetry for map page view (best-effort)
|
968 |
-
(function() {
|
969 |
-
try {
|
970 |
-
document.addEventListener('DOMContentLoaded', function() {
|
971 |
-
var token = localStorage.getItem('auth_token');
|
972 |
-
if (!token) return;
|
973 |
-
fetch('/api/telemetry', {
|
974 |
-
method: 'POST',
|
975 |
-
headers: {
|
976 |
-
'Content-Type': 'application/json',
|
977 |
-
'Authorization': 'Bearer ' + token
|
978 |
-
},
|
979 |
-
body: JSON.stringify({
|
980 |
-
event_type: 'ui',
|
981 |
-
status: 'view',
|
982 |
-
metadata: { page: 'map', action: 'load' }
|
983 |
-
})
|
984 |
-
}).catch(function(){});
|
985 |
-
});
|
986 |
-
} catch(e) { /* ignore */ }
|
987 |
-
})();
|
988 |
-
</script>
|
989 |
</body>
|
990 |
</html>
|
|
|
13 |
<!-- Leaflet CSS -->
|
14 |
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
|
15 |
<link rel="stylesheet" href="/static/css/design-system.css">
|
|
|
|
|
|
|
16 |
<style>
|
17 |
|
18 |
:root {
|
|
|
99 |
overflow: hidden;
|
100 |
}
|
101 |
|
102 |
+
/* Ensure header content is above background */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
.tt-header-content {
|
104 |
position: relative;
|
105 |
z-index: 2;
|
|
|
799 |
// Force refresh if we detect cached version
|
800 |
(function() {
|
801 |
const currentVersion = '5.1.1';
|
802 |
+
const timestamp = '1755115883'; // Current timestamp for cache busting
|
803 |
const lastVersion = sessionStorage.getItem('treetrack_version');
|
804 |
const lastTimestamp = sessionStorage.getItem('treetrack_timestamp');
|
805 |
|
|
|
925 |
|
926 |
<!-- Leaflet JS -->
|
927 |
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
|
928 |
+
<script src="/static/map.js?v=5.1.1&t=1755115883">
|
929 |
|
930 |
"default-state": {
|
931 |
gradients: [
|
|
|
940 |
}
|
941 |
});
|
942 |
|
|
|
943 |
// Defer any heavy data fetch; ensure map shell is visible first
|
944 |
if (window.requestAnimationFrame) {
|
945 |
requestAnimationFrame(() => setTimeout(() => {
|
|
|
948 |
}
|
949 |
});
|
950 |
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
951 |
</body>
|
952 |
</html>
|
static/sw.js
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
// TreeTrack Service Worker - PWA and Offline Support
|
2 |
-
const VERSION =
|
3 |
const CACHE_NAME = `treetrack-v${VERSION}`;
|
4 |
const STATIC_CACHE = `static-v${VERSION}`;
|
5 |
const API_CACHE = `api-v${VERSION}`;
|
|
|
1 |
// TreeTrack Service Worker - PWA and Offline Support
|
2 |
+
const VERSION = 1755115883; // Cache busting bump - force clients to fetch new static assets and header image change
|
3 |
const CACHE_NAME = `treetrack-v${VERSION}`;
|
4 |
const STATIC_CACHE = `static-v${VERSION}`;
|
5 |
const API_CACHE = `api-v${VERSION}`;
|
static/telemetry.html
CHANGED
@@ -1,179 +1 @@
|
|
1 |
-
<!DOCTYPE html>
|
2 |
-
<html lang="en">
|
3 |
-
<head>
|
4 |
-
<meta charset="UTF-8" />
|
5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6 |
-
<title>Telemetry Viewer - Admin</title>
|
7 |
-
<link rel="stylesheet" href="/static/css/design-system.css" />
|
8 |
-
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
9 |
-
<meta http-equiv="Pragma" content="no-cache">
|
10 |
-
<meta http-equiv="Expires" content="0">
|
11 |
-
<style>
|
12 |
-
body { font-family: Inter, system-ui, -apple-system, Segoe UI, sans-serif; background: var(--gray-50); color: var(--gray-800); }
|
13 |
-
.container { max-width: 1200px; margin: 0 auto; padding: 1rem; }
|
14 |
-
.header { display: flex; justify-content: space-between; align-items: center; margin: 1rem 0; }
|
15 |
-
.title { font-size: 1.25rem; font-weight: 700; }
|
16 |
-
.controls { display: flex; gap: .5rem; align-items: center; }
|
17 |
-
.filter-input { padding: .5rem .75rem; border: 1px solid var(--gray-300); border-radius: .5rem; font-size: .9rem; }
|
18 |
-
.btn { padding: .5rem .75rem; border: 1px solid var(--gray-300); background: white; border-radius: .5rem; cursor: pointer; }
|
19 |
-
.btn-primary { background: var(--primary-600); color: white; border-color: var(--primary-600); }
|
20 |
-
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
21 |
-
.meta { color: var(--gray-500); font-size: .85rem; }
|
22 |
-
.table { width: 100%; border-collapse: collapse; background: white; border: 1px solid var(--gray-200); border-radius: .75rem; overflow: hidden; }
|
23 |
-
.table th, .table td { padding: .5rem .75rem; border-bottom: 1px solid var(--gray-100); text-align: left; vertical-align: top; font-size: .9rem; }
|
24 |
-
.table th { background: var(--gray-50); font-weight: 600; }
|
25 |
-
.pill { display: inline-block; padding: .15rem .4rem; border-radius: .5rem; font-size: .75rem; border: 1px solid var(--gray-300); }
|
26 |
-
.pill-success { background: var(--green-50); color: var(--green-700); border-color: var(--green-300); }
|
27 |
-
.pill-error { background: var(--red-50); color: var(--red-700); border-color: var(--red-300); }
|
28 |
-
.pill-start { background: var(--orange-50); color: var(--orange-700); border-color: var(--orange-300); }
|
29 |
-
.code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .8rem; background: var(--gray-50); padding: .35rem .5rem; border-radius: .35rem; border: 1px solid var(--gray-200); display: inline-block; max-width: 520px; overflow: auto; }
|
30 |
-
.row { transition: background .2s ease; }
|
31 |
-
.row:hover { background: var(--gray-50); }
|
32 |
-
.footer { display: flex; justify-content: space-between; align-items: center; margin-top: .75rem; }
|
33 |
-
</style>
|
34 |
-
</head>
|
35 |
-
<body>
|
36 |
-
<div class="container">
|
37 |
-
<div class="header">
|
38 |
-
<div>
|
39 |
-
<div class="title">Telemetry Viewer (Admin)</div>
|
40 |
-
<div class="meta">Inspect recent telemetry events to monitor app health and user actions.</div>
|
41 |
-
</div>
|
42 |
-
<div class="controls">
|
43 |
-
<input id="limit" class="filter-input" type="number" min="1" max="1000" value="200" title="Max events" />
|
44 |
-
<input id="search" class="filter-input" placeholder="Search text (type, status, user, etc.)" />
|
45 |
-
<button id="refresh" class="btn btn-primary">Refresh</button>
|
46 |
-
<a class="btn" href="/">Back</a>
|
47 |
-
</div>
|
48 |
-
</div>
|
49 |
-
|
50 |
-
<div id="meta" class="meta" style="margin-bottom: .5rem;"> </div>
|
51 |
-
|
52 |
-
<table class="table">
|
53 |
-
<thead>
|
54 |
-
<tr>
|
55 |
-
<th style="width: 10rem;">Timestamp</th>
|
56 |
-
<th style="width: 8rem;">Type</th>
|
57 |
-
<th style="width: 8rem;">Status</th>
|
58 |
-
<th style="width: 10rem;">User</th>
|
59 |
-
<th>Metadata</th>
|
60 |
-
</tr>
|
61 |
-
</thead>
|
62 |
-
<tbody id="tbody"></tbody>
|
63 |
-
</table>
|
64 |
-
|
65 |
-
<div class="footer">
|
66 |
-
<div id="source" class="meta"></div>
|
67 |
-
<div class="meta">Only accessible to admin users</div>
|
68 |
-
</div>
|
69 |
-
</div>
|
70 |
-
|
71 |
-
<script type="module">
|
72 |
-
async function fetchTelemetry(limit) {
|
73 |
-
const params = new URLSearchParams({ limit: String(limit) });
|
74 |
-
const res = await fetch(`/api/telemetry?${params.toString()}`, { headers: authHeaders() });
|
75 |
-
if (res.status === 401 || res.status === 403) {
|
76 |
-
alert('Unauthorized. You must be an admin to view telemetry.');
|
77 |
-
window.location.href = '/login';
|
78 |
-
return null;
|
79 |
-
}
|
80 |
-
if (!res.ok) {
|
81 |
-
throw new Error('Failed to fetch telemetry');
|
82 |
-
}
|
83 |
-
return res.json();
|
84 |
-
}
|
85 |
-
|
86 |
-
function authHeaders() {
|
87 |
-
const token = localStorage.getItem('auth_token');
|
88 |
-
return token ? { 'Authorization': `Bearer ${token}` } : {};
|
89 |
-
}
|
90 |
-
|
91 |
-
function pill(status) {
|
92 |
-
if (!status) return '';
|
93 |
-
const cls = status === 'success' ? 'pill-success' : status === 'error' ? 'pill-error' : status === 'start' ? 'pill-start' : '';
|
94 |
-
return `<span class="pill ${cls}">${status}</span>`;
|
95 |
-
}
|
96 |
-
|
97 |
-
function escapeHtml(s) { return s.replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c])); }
|
98 |
-
|
99 |
-
function renderRows(events) {
|
100 |
-
const tbody = document.getElementById('tbody');
|
101 |
-
const q = (document.getElementById('search').value || '').toLowerCase().trim();
|
102 |
-
tbody.innerHTML = '';
|
103 |
-
for (const evt of events) {
|
104 |
-
const ts = evt.timestamp || '';
|
105 |
-
const type = evt.event_type || '';
|
106 |
-
const status = evt.status || '';
|
107 |
-
const user = (evt.user && evt.user.username) ? `${evt.user.username} (${evt.user.role || ''})` : '';
|
108 |
-
const meta = evt.metadata ? escapeHtml(JSON.stringify(evt.metadata)) : '';
|
109 |
-
const line = `${ts} ${type} ${status} ${user} ${meta}`.toLowerCase();
|
110 |
-
if (q && !line.includes(q)) continue;
|
111 |
-
const tr = document.createElement('tr');
|
112 |
-
tr.className = 'row';
|
113 |
-
tr.innerHTML = `
|
114 |
-
<td>${escapeHtml(ts)}</td>
|
115 |
-
<td><span class="pill">${escapeHtml(type)}</span></td>
|
116 |
-
<td>${pill(status)}</td>
|
117 |
-
<td>${escapeHtml(user)}</td>
|
118 |
-
<td><span class="code">${meta}</span></td>
|
119 |
-
`;
|
120 |
-
tbody.appendChild(tr);
|
121 |
-
}
|
122 |
-
}
|
123 |
-
|
124 |
-
async function refresh() {
|
125 |
-
const limit = Math.max(1, Math.min(1000, parseInt(document.getElementById('limit').value || '200', 10)));
|
126 |
-
document.getElementById('refresh').disabled = true;
|
127 |
-
try {
|
128 |
-
const data = await fetchTelemetry(limit);
|
129 |
-
if (!data) return;
|
130 |
-
document.getElementById('meta').textContent = `Loaded ${data.count} events`;
|
131 |
-
document.getElementById('source').textContent = `Source: ${data.source}`;
|
132 |
-
renderRows(data.events || []);
|
133 |
-
} catch (e) {
|
134 |
-
alert(e.message || 'Failed to load telemetry');
|
135 |
-
} finally {
|
136 |
-
document.getElementById('refresh').disabled = false;
|
137 |
-
}
|
138 |
-
}
|
139 |
-
|
140 |
-
document.getElementById('refresh').addEventListener('click', refresh);
|
141 |
-
document.getElementById('search').addEventListener('input', () => {
|
142 |
-
// quick re-filter without fetching again
|
143 |
-
const meta = document.getElementById('meta').textContent || '';
|
144 |
-
// no-op; filtering reads from table content
|
145 |
-
const rows = Array.from(document.querySelectorAll('#tbody tr'));
|
146 |
-
const q = (document.getElementById('search').value || '').toLowerCase().trim();
|
147 |
-
rows.forEach(row => {
|
148 |
-
const text = row.textContent.toLowerCase();
|
149 |
-
row.style.display = q && !text.includes(q) ? 'none' : '';
|
150 |
-
});
|
151 |
-
});
|
152 |
-
|
153 |
-
// On load: validate auth and fetch
|
154 |
-
(async () => {
|
155 |
-
try {
|
156 |
-
const token = localStorage.getItem('auth_token');
|
157 |
-
if (!token) {
|
158 |
-
window.location.href = '/login';
|
159 |
-
return;
|
160 |
-
}
|
161 |
-
// Validate user and ensure admin
|
162 |
-
const res = await fetch('/api/auth/validate', { headers: authHeaders() });
|
163 |
-
if (!res.ok) { window.location.href = '/login'; return; }
|
164 |
-
const info = await res.json();
|
165 |
-
const perms = (info.user && info.user.permissions) || [];
|
166 |
-
if (!perms.includes('admin')) {
|
167 |
-
alert('Admin access required');
|
168 |
-
window.location.href = '/';
|
169 |
-
return;
|
170 |
-
}
|
171 |
-
await refresh();
|
172 |
-
} catch {
|
173 |
-
window.location.href = '/login';
|
174 |
-
}
|
175 |
-
})();
|
176 |
-
</script>
|
177 |
-
</body>
|
178 |
-
</html>
|
179 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
|
version.json
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
{
|
2 |
"version": "5.1.1",
|
3 |
-
"timestamp":
|
4 |
}
|
|
|
1 |
{
|
2 |
"version": "5.1.1",
|
3 |
+
"timestamp": 1755115883
|
4 |
}
|