cloudfrontize 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +65 -22
  2. package/dist/cli.js +36 -32
  3. package/dist/ui/app.js +442 -0
  4. package/dist/ui/assets/cloudfrontize-pro-transparent-512.png +0 -0
  5. package/dist/ui/favicons/android-icon-144x144.png +0 -0
  6. package/dist/ui/favicons/android-icon-192x192.png +0 -0
  7. package/dist/ui/favicons/android-icon-36x36.png +0 -0
  8. package/dist/ui/favicons/android-icon-48x48.png +0 -0
  9. package/dist/ui/favicons/android-icon-72x72.png +0 -0
  10. package/dist/ui/favicons/android-icon-96x96.png +0 -0
  11. package/dist/ui/favicons/apple-icon-114x114.png +0 -0
  12. package/dist/ui/favicons/apple-icon-120x120.png +0 -0
  13. package/dist/ui/favicons/apple-icon-144x144.png +0 -0
  14. package/dist/ui/favicons/apple-icon-152x152.png +0 -0
  15. package/dist/ui/favicons/apple-icon-180x180.png +0 -0
  16. package/dist/ui/favicons/apple-icon-57x57.png +0 -0
  17. package/dist/ui/favicons/apple-icon-60x60.png +0 -0
  18. package/dist/ui/favicons/apple-icon-72x72.png +0 -0
  19. package/dist/ui/favicons/apple-icon-76x76.png +0 -0
  20. package/dist/ui/favicons/apple-icon-precomposed.png +0 -0
  21. package/dist/ui/favicons/apple-icon.png +0 -0
  22. package/dist/ui/favicons/browserconfig.xml +11 -0
  23. package/dist/ui/favicons/favicon-16x16.png +0 -0
  24. package/dist/ui/favicons/favicon-32x32.png +0 -0
  25. package/dist/ui/favicons/favicon-96x96.png +0 -0
  26. package/dist/ui/favicons/favicon.ico +0 -0
  27. package/dist/ui/favicons/manifest.json +41 -0
  28. package/dist/ui/favicons/ms-icon-144x144.png +0 -0
  29. package/dist/ui/favicons/ms-icon-150x150.png +0 -0
  30. package/dist/ui/favicons/ms-icon-310x310.png +0 -0
  31. package/dist/ui/favicons/ms-icon-70x70.png +0 -0
  32. package/dist/ui/index.html +129 -0
  33. package/dist/ui/style.css +792 -0
  34. package/package.json +1 -1
package/dist/ui/app.js ADDED
@@ -0,0 +1,442 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CloudFrontize Developer UI - Client Logic
5
+ */
6
+
7
+ document.addEventListener('DOMContentLoaded', () => {
8
+ const requestFeed = document.getElementById('request-feed');
9
+ const portDisplay = document.getElementById('current-port');
10
+
11
+ // Tab Switching
12
+ const tabBtns = document.querySelectorAll('.tab-btn');
13
+ const tabContents = document.querySelectorAll('.tab-content');
14
+
15
+ tabBtns.forEach(btn => {
16
+ btn.addEventListener('click', () => {
17
+ tabBtns.forEach(b => b.classList.remove('active'));
18
+ tabContents.forEach(c => c.classList.remove('active'));
19
+
20
+ btn.classList.add('active');
21
+ document.getElementById(btn.dataset.tab).classList.add('active');
22
+ });
23
+ });
24
+
25
+ // Filtering Pulse
26
+ const filterBtns = document.querySelectorAll('.btn-filter');
27
+ filterBtns.forEach(btn => {
28
+ btn.addEventListener('click', () => {
29
+ filterBtns.forEach(b => b.classList.remove('active'));
30
+ btn.classList.add('active');
31
+
32
+ const type = btn.dataset.type;
33
+ const cards = document.querySelectorAll('.request-card');
34
+ cards.forEach(card => {
35
+ if (!type || type === 'all') {
36
+ card.style.display = 'block';
37
+ } else if (type === 'rewrite') {
38
+ card.style.display = card.classList.contains('is-rewrite') ? 'block' : 'none';
39
+ } else if (type === 'violation') {
40
+ card.style.display = card.classList.contains('has-violation') ? 'block' : 'none';
41
+ }
42
+ });
43
+ });
44
+ });
45
+
46
+ // Real-time Feed via SSE
47
+ const evtSource = new EventSource('/events');
48
+
49
+ evtSource.onmessage = (event) => {
50
+ const data = JSON.parse(event.data);
51
+ if (data.type === 'init') {
52
+ portDisplay.textContent = data.port;
53
+
54
+ // Dynamic Versioning
55
+ const versionTag = document.querySelector('.version-tag');
56
+ if (versionTag && data.version) {
57
+ versionTag.textContent = `Developer Edition v${data.version}`;
58
+ }
59
+
60
+ // Wipe all local state for a clean slate
61
+ requestFeed.innerHTML = '<div class="empty-state"><p>Waiting for requests...</p></div>';
62
+ loadHeaderState(data.headerState);
63
+
64
+ // Reset "Dirty" state
65
+ markClean();
66
+
67
+ // Reset filters to "All"
68
+ const allFilterBtn = document.querySelector('.btn-filter[data-type="all"]');
69
+ if (allFilterBtn) {
70
+ filterBtns.forEach(b => b.classList.remove('active'));
71
+ allFilterBtn.classList.add('active');
72
+ }
73
+ } else if (data.type === 'request') {
74
+ addRequestToFeed(data.request);
75
+ }
76
+ };
77
+
78
+ evtSource.onerror = () => {
79
+ console.error("SSE Connection lost.");
80
+ };
81
+
82
+ const sidebar = document.querySelector('.sidebar');
83
+ const applyBtn = document.getElementById('apply-headers');
84
+ let isDirty = false;
85
+
86
+ function markDirty() {
87
+ if (isDirty) return;
88
+ isDirty = true;
89
+ applyBtn.classList.add('dirty');
90
+ applyBtn.innerText = 'Save Changes';
91
+ }
92
+
93
+ function markClean() {
94
+ isDirty = false;
95
+ applyBtn.classList.remove('dirty');
96
+ applyBtn.innerText = 'Applied';
97
+ }
98
+
99
+ // Header Management
100
+ function createHeaderRow(key = '', value = '', isDelete = false) {
101
+ const row = document.createElement('div');
102
+ row.className = 'header-row' + (isDelete ? ' suppressed' : '');
103
+ row.innerHTML = `
104
+ <div class="row-main">
105
+ <input type="text" placeholder="Header Name" value="${key}" class="hdr-key">
106
+ <input type="text" placeholder="Value" value="${value}" class="hdr-val" ${isDelete ? 'disabled' : ''}>
107
+ </div>
108
+ <div class="row-actions">
109
+ <button class="btn-toggle-action ${isDelete ? '' : 'active'}" title="${isDelete ? 'Set: Override/Inject' : 'Suppress: Explicitly Delete'}">
110
+ <span class="material-icons">${isDelete ? 'do_not_disturb_on' : 'verified'}</span>
111
+ </button>
112
+ <button class="btn-remove" title="Remove"><span class="material-icons">delete</span></button>
113
+ </div>
114
+ `;
115
+
116
+ const actionBtn = row.querySelector('.btn-toggle-action');
117
+ const valInput = row.querySelector('.hdr-val');
118
+ const keyInput = row.querySelector('.hdr-key');
119
+
120
+ const handleChange = () => markDirty();
121
+ keyInput.oninput = handleChange;
122
+ valInput.oninput = handleChange;
123
+
124
+ actionBtn.onclick = () => {
125
+ const nowActive = actionBtn.classList.toggle('active');
126
+ const isSuppressed = !nowActive;
127
+
128
+ row.classList.toggle('suppressed', isSuppressed);
129
+ actionBtn.title = isSuppressed ? 'Set: Override/Inject' : 'Suppress: Explicitly Delete';
130
+ actionBtn.innerHTML = `<span class="material-icons">${isSuppressed ? 'do_not_disturb_on' : 'verified'}</span>`;
131
+ valInput.disabled = isSuppressed;
132
+ markDirty();
133
+ };
134
+
135
+ row.querySelector('.btn-remove').onclick = () => {
136
+ row.remove();
137
+ markDirty();
138
+ };
139
+ return row;
140
+ }
141
+
142
+ document.getElementById('add-req-header').onclick = () => {
143
+ document.getElementById('req-header-list').appendChild(createHeaderRow());
144
+ markDirty();
145
+ };
146
+
147
+ document.getElementById('add-res-header').onclick = () => {
148
+ document.getElementById('res-header-list').appendChild(createHeaderRow());
149
+ markDirty();
150
+ };
151
+
152
+ // Presets
153
+ document.querySelectorAll('.btn-preset').forEach(btn => {
154
+ btn.onclick = () => {
155
+ const listId = btn.dataset.type === 'request' ? 'req-header-list' : 'res-header-list';
156
+ const list = document.getElementById(listId);
157
+ list.appendChild(createHeaderRow(btn.dataset.header, btn.dataset.value));
158
+ markDirty();
159
+ };
160
+ });
161
+
162
+ document.getElementById('reset-headers').onclick = () => {
163
+ if (confirm('Reset all overrides to file defaults?')) {
164
+ loadHeaderState({}); // We would fetch defaults if we had them saved, but for now we clear
165
+ markDirty();
166
+ }
167
+ };
168
+
169
+ // Save State
170
+ applyBtn.onclick = async () => {
171
+ const state = { request: {}, response: {} };
172
+ let hasValidationError = false;
173
+
174
+ const collect = (listId, target) => {
175
+ document.querySelectorAll(`#${listId} .header-row`).forEach(row => {
176
+ const keyInput = row.querySelector('.hdr-key');
177
+ const valInput = row.querySelector('.hdr-val');
178
+ const k = keyInput.value.trim();
179
+ const v = valInput.value.trim();
180
+ const isSuppressed = row.classList.contains('suppressed');
181
+
182
+ if (!k) {
183
+ // Any dangling row must have a name, or it's a validation error
184
+ keyInput.classList.add('input-error');
185
+ hasValidationError = true;
186
+ return;
187
+ }
188
+
189
+ keyInput.classList.remove('input-error');
190
+ target[k] = isSuppressed ? null : v;
191
+ });
192
+ };
193
+
194
+ collect('req-header-list', state.request);
195
+ collect('res-header-list', state.response);
196
+
197
+ if (hasValidationError) {
198
+ applyBtn.innerText = '🛑 Fix Header Names';
199
+ applyBtn.classList.add('error-pulse');
200
+ setTimeout(() => {
201
+ applyBtn.innerText = isDirty ? 'Save Changes' : 'Applied';
202
+ applyBtn.classList.remove('error-pulse');
203
+ }, 3000);
204
+ return;
205
+ }
206
+
207
+ try {
208
+ const res = await fetch('/headers', {
209
+ method: 'POST',
210
+ headers: { 'Content-Type': 'application/json' },
211
+ body: JSON.stringify(state)
212
+ });
213
+ if (res.ok) {
214
+ markClean();
215
+ applyBtn.innerText = '✅ Applied';
216
+ applyBtn.classList.add('success');
217
+ setTimeout(() => {
218
+ applyBtn.innerText = 'Applied';
219
+ applyBtn.classList.remove('success');
220
+ }, 2000);
221
+ }
222
+ } catch (err) {
223
+ alert('🛑 Failed to save headers.');
224
+ }
225
+ };
226
+
227
+ function loadHeaderState(state) {
228
+ const reqList = document.getElementById('req-header-list');
229
+ const resList = document.getElementById('res-header-list');
230
+ reqList.innerHTML = '';
231
+ resList.innerHTML = '';
232
+
233
+ Object.entries(state.request || {}).forEach(([k, v]) => reqList.appendChild(createHeaderRow(k, v, v === null)));
234
+ Object.entries(state.response || {}).forEach(([k, v]) => resList.appendChild(createHeaderRow(k, v, v === null)));
235
+ }
236
+
237
+ function addRequestToFeed(req) {
238
+ if (document.querySelector('.empty-state')) {
239
+ requestFeed.innerHTML = '';
240
+ }
241
+
242
+ const card = document.createElement('div');
243
+ card.className = 'request-card';
244
+ card.id = `req-${req.id}`;
245
+ if (req.violation) card.classList.add('has-violation');
246
+ if (req.steps && req.steps.length > 1) card.classList.add('is-rewrite');
247
+
248
+ const statusClass = req.status >= 500 ? 'status-error' : (req.status >= 400 ? 'status-warn' : 'status-ok');
249
+
250
+ card.innerHTML = `
251
+ <div class="card-header">
252
+ <div style="display: flex; align-items: center; gap: 0.75rem">
253
+ <span class="card-id">#${req.id.slice(0, 8)}</span>
254
+ <span class="card-method">${req.method}</span>
255
+ <span class="card-uri">${req.path}</span>
256
+ </div>
257
+ <div style="display: flex; align-items: center; gap: 1rem">
258
+ <span class="card-status ${statusClass}">${req.status}</span>
259
+ <span class="expand-icon">expand_more</span>
260
+ </div>
261
+ </div>
262
+ <div class="card-body-mini">
263
+ <div class="rewrite-path">${req.steps.length > 1 ? `↳ ${req.steps[req.steps.length - 1].uri}` : ''}</div>
264
+ <div class="performance-mini">${req.cpu.toFixed(2)}ms</div>
265
+ </div>
266
+ <div class="card-details" style="display: none;">
267
+ <div class="loading-spinner">Loading deep inspection data...</div>
268
+ </div>
269
+ `;
270
+
271
+ card.querySelector('.expand-icon').onclick = (e) => {
272
+ e.stopPropagation();
273
+ toggleRequestDetails(req.id, card);
274
+ };
275
+ requestFeed.prepend(card);
276
+
277
+ if (requestFeed.children.length > 50) {
278
+ requestFeed.removeChild(requestFeed.lastChild);
279
+ }
280
+ }
281
+
282
+ async function toggleRequestDetails(id, card) {
283
+ const detailsEl = card.querySelector('.card-details');
284
+ const iconEl = card.querySelector('.expand-icon');
285
+ const isExpanded = detailsEl.style.display === 'block';
286
+
287
+ if (isExpanded) {
288
+ detailsEl.style.display = 'none';
289
+ card.classList.remove('expanded');
290
+ iconEl.innerText = 'expand_more';
291
+ return;
292
+ }
293
+
294
+ detailsEl.style.display = 'block';
295
+ card.classList.add('expanded');
296
+ iconEl.innerText = 'expand_less';
297
+
298
+ if (detailsEl.innerHTML.includes('Loading deep inspection data...')) {
299
+ try {
300
+ const res = await fetch(`/request/${id}`);
301
+ const data = await res.json();
302
+ renderDetails(detailsEl, data);
303
+ } catch (err) {
304
+ detailsEl.innerHTML = `<div class="error">Failed to load details: ${err.message}</div>`;
305
+ }
306
+ }
307
+ }
308
+
309
+ function renderDetails(el, data) {
310
+ el.innerHTML = `
311
+ <div class="detail-nav">
312
+ <div class="nav-item active" data-pane="trace">Trace</div>
313
+ <div class="nav-item" data-pane="headers">Headers</div>
314
+ <div class="nav-item" data-pane="body">Body</div>
315
+ </div>
316
+
317
+ <div class="detail-pane active" id="pane-trace-${data.id}">
318
+ <div class="detail-grid">
319
+ <div class="detail-section">
320
+ <h4>Pipeline Evolution</h4>
321
+ <div class="shadow-rewrite">
322
+ ${data.steps.map(s => `
323
+ <div class="step">
324
+ <span>${s.uri}</span>
325
+ <span class="step-label">${s.label}</span>
326
+ </div>
327
+ `).join('')}
328
+ </div>
329
+ ${data.violation ? `
330
+ <div class="fidelity-alert">
331
+ <span>🛑</span>
332
+ <div>
333
+ <strong>Fidelity Violation:</strong> ${data.violation}
334
+ </div>
335
+ </div>
336
+ ` : ''}
337
+ </div>
338
+ </div>
339
+ </div>
340
+
341
+ <div class="detail-pane" id="pane-headers-${data.id}">
342
+ <div class="header-lifecycle">
343
+ <div class="lifecycle-col">
344
+ <h5>Request lifecycle (Viewer → Origin)</h5>
345
+ <div class="header-diff">
346
+ ${renderHeaderDiff(data.headers.request.viewer, data.headers.request.origin)}
347
+ </div>
348
+ </div>
349
+ <div class="lifecycle-col">
350
+ <h5>Response lifecycle (Origin → Viewer)</h5>
351
+ <div class="header-diff">
352
+ ${renderHeaderDiff(data.headers.response.origin, data.headers.response.viewer)}
353
+ </div>
354
+ </div>
355
+ </div>
356
+ </div>
357
+
358
+ <div class="detail-pane" id="pane-body-${data.id}">
359
+ <div class="detail-section">
360
+ <h4>Body Snippet (First 1KB)</h4>
361
+ <pre class="body-snippet">${data.bodySnippet || '(No body content buffered)'}</pre>
362
+ </div>
363
+ </div>
364
+ `;
365
+
366
+ // Wire up internal tabs
367
+ el.querySelectorAll('.nav-item').forEach(nav => {
368
+ nav.onclick = (e) => {
369
+ e.stopPropagation();
370
+ el.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
371
+ el.querySelectorAll('.detail-pane').forEach(p => p.classList.remove('active'));
372
+
373
+ nav.classList.add('active');
374
+ el.querySelector(`#pane-${nav.dataset.pane}-${data.id}`).classList.add('active');
375
+ };
376
+ });
377
+ }
378
+
379
+ function renderHeaderDiff(incoming, final) {
380
+ const allKeys = new Set([...Object.keys(incoming), ...Object.keys(final)]);
381
+ let html = '<table class="diff-table">';
382
+
383
+ [...allKeys].sort().forEach(key => {
384
+ const val1 = incoming[key];
385
+ const val2 = final[key];
386
+
387
+ let rowClass = '';
388
+ if (val1 === undefined) rowClass = 'h-added';
389
+ else if (val2 === undefined) rowClass = 'h-removed';
390
+ else if (JSON.stringify(val1) !== JSON.stringify(val2)) rowClass = 'h-modified';
391
+
392
+ html += `
393
+ <tr class="${rowClass}">
394
+ <td class="h-key">${key}</td>
395
+ <td class="h-val">${val2 !== undefined ? val2 : '<span class="deleted">REMOVED</span>'}</td>
396
+ </tr>
397
+ `;
398
+ });
399
+
400
+ return html + '</table>';
401
+ }
402
+
403
+ document.getElementById('clear-feed').onclick = () => {
404
+ requestFeed.innerHTML = '<div class="empty-state"><p>Waiting for requests...</p></div>';
405
+ };
406
+
407
+ // About Modal Logic
408
+ const aboutModal = document.getElementById('about-modal');
409
+ const aboutTrigger = document.getElementById('about-trigger');
410
+ const closeAbout = document.getElementById('close-about');
411
+
412
+ const showAbout = () => {
413
+ aboutModal.classList.add('active');
414
+ document.body.style.overflow = 'hidden'; // Prevent background scroll
415
+ };
416
+
417
+ const hideAbout = () => {
418
+ aboutModal.classList.remove('active');
419
+ document.body.style.overflow = '';
420
+ };
421
+
422
+ aboutTrigger.addEventListener('click', (e) => {
423
+ e.preventDefault();
424
+ showAbout();
425
+ });
426
+
427
+ closeAbout.addEventListener('click', hideAbout);
428
+
429
+ // Close on click outside
430
+ aboutModal.addEventListener('click', (e) => {
431
+ if (e.target === aboutModal) {
432
+ hideAbout();
433
+ }
434
+ });
435
+
436
+ // Close on Escape
437
+ document.addEventListener('keydown', (e) => {
438
+ if (e.key === 'Escape' && aboutModal.classList.contains('active')) {
439
+ hideAbout();
440
+ }
441
+ });
442
+ });
Binary file
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <browserconfig>
3
+ <msapplication>
4
+ <tile>
5
+ <square70x70logo src="/favicons/ms-icon-70x70.png"/>
6
+ <square150x150logo src="/favicons/ms-icon-150x150.png"/>
7
+ <square310x310logo src="/favicons/ms-icon-310x310.png"/>
8
+ <TileColor>#ffffff</TileColor>
9
+ </tile>
10
+ </msapplication>
11
+ </browserconfig>
Binary file
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "App",
3
+ "icons": [
4
+ {
5
+ "src": "/favicons/android-icon-36x36.png",
6
+ "sizes": "36x36",
7
+ "type": "image/png",
8
+ "density": "0.75"
9
+ },
10
+ {
11
+ "src": "/favicons/android-icon-48x48.png",
12
+ "sizes": "48x48",
13
+ "type": "image/png",
14
+ "density": "1.0"
15
+ },
16
+ {
17
+ "src": "/favicons/android-icon-72x72.png",
18
+ "sizes": "72x72",
19
+ "type": "image/png",
20
+ "density": "1.5"
21
+ },
22
+ {
23
+ "src": "/favicons/android-icon-96x96.png",
24
+ "sizes": "96x96",
25
+ "type": "image/png",
26
+ "density": "2.0"
27
+ },
28
+ {
29
+ "src": "/favicons/android-icon-144x144.png",
30
+ "sizes": "144x144",
31
+ "type": "image/png",
32
+ "density": "3.0"
33
+ },
34
+ {
35
+ "src": "/favicons/android-icon-192x192.png",
36
+ "sizes": "192x192",
37
+ "type": "image/png",
38
+ "density": "4.0"
39
+ }
40
+ ]
41
+ }