devendergarg14 commited on
Commit
6541ee1
·
verified ·
1 Parent(s): 0a0bd8b

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +77 -48
index.html CHANGED
@@ -8,7 +8,6 @@
8
  <meta name="theme-color" content="#2c303a" media="(prefers-color-scheme: dark)">
9
  <script src="https://cdn.tailwindcss.com"></script>
10
  <style>
11
- /* Custom styles (UNCHANGED from your original - Retained for brevity) */
12
  :root {
13
  --light-bg: #e0e5ec; --dark-bg: #2c303a; --light-shadow-outer-1: #a3b1c6;
14
  --light-shadow-outer-2: #ffffff; --dark-shadow-outer-1: #22252e; --dark-shadow-outer-2: #363a46;
@@ -117,33 +116,51 @@
117
  const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
118
  const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
119
 
120
- let monitoredUrlsCache = []; // Client-side cache of URL data from backend
121
-
122
- const UI_REFRESH_INTERVAL_MS = 5000; // Refresh UI data from backend every 5 seconds
123
- const HISTORY_DURATION_MS_FOR_DISPLAY = 60 * 60 * 1000; // For uptime % and history bar visualization
124
  const MAX_HISTORY_POINTS_FOR_DISPLAY = 90;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- // --- Theme Toggle (UNCHANGED logic) ---
127
  function applyTheme(isDark) {
128
  if (isDark) {
129
  document.documentElement.classList.add('dark');
130
  themeToggleLightIcon.classList.remove('hidden');
131
  themeToggleDarkIcon.classList.add('hidden');
132
- requestAnimationFrame(() => {
133
- const darkBg = getComputedStyle(document.documentElement).getPropertyValue('--dark-bg').trim();
134
- document.querySelector('meta[name="theme-color"][media="(prefers-color-scheme: dark)"]').setAttribute('content', darkBg);
135
- });
136
  } else {
137
  document.documentElement.classList.remove('dark');
138
  themeToggleLightIcon.classList.add('hidden');
139
  themeToggleDarkIcon.classList.remove('hidden');
140
- requestAnimationFrame(() => {
141
- const lightBg = getComputedStyle(document.documentElement).getPropertyValue('--light-bg').trim();
142
- document.querySelector('meta[name="theme-color"]:not([media])').setAttribute('content', lightBg);
143
- });
144
  }
 
145
  requestAnimationFrame(() => {
146
- document.body.style.backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--light-bg').trim();
 
 
 
147
  });
148
  }
149
  function toggleTheme() {
@@ -162,39 +179,59 @@
162
 
163
  // --- API Communication Wrapper ---
164
  async function apiRequest(endpoint, options = {}) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  try {
166
- const response = await fetch(endpoint, options);
167
  if (!response.ok) {
168
  const errorData = await response.json().catch(() => ({ message: response.statusText }));
169
  throw new Error(errorData.error || errorData.message || `HTTP Error ${response.status}`);
170
  }
171
  if (response.status === 204 || response.headers.get("content-length") === "0") {
172
- return null; // Handle No Content responses
173
  }
174
  return await response.json();
175
  } catch (error) {
176
  console.error(`API Request Error for ${endpoint}:`, error);
177
- errorMsg.textContent = `Error: ${error.message}`; // Display error to user
178
- throw error; // Re-throw for calling function to handle if needed
179
  }
180
  }
181
 
182
  async function fetchAndRenderUrls() {
183
  try {
184
- const dataFromServer = await apiRequest('/api/urls');
185
- monitoredUrlsCache = dataFromServer || []; // API might return empty list or null
186
  renderUrlListUI();
187
  } catch (e) {
188
- // Error already logged by apiRequest and potentially shown in errorMsg
189
- // Optionally, render a more specific error state for the list
190
- urlList.innerHTML = `<p class="italic" style="color: var(--text-light-muted);">Could not load URLs. Server may be down.</p>`;
191
  }
192
  }
193
 
194
- // --- Uptime and History Bar Calculation (for UI) ---
195
  function getDisplayMetrics(backendHistoryArray) {
196
- const historyForDisplay = (backendHistoryArray || []).map(h => ({ ...h, timestamp: h.timestamp * 1000 })); // Convert backend sec to ms
197
-
198
  const cutoffTimeMs = Date.now() - HISTORY_DURATION_MS_FOR_DISPLAY;
199
  const relevantHistory = historyForDisplay.filter(entry => entry.timestamp >= cutoffTimeMs);
200
 
@@ -202,13 +239,10 @@
202
 
203
  const okCount = relevantHistory.filter(entry => entry.status === 'ok').length;
204
  const uptimePercent = Math.round((okCount / relevantHistory.length) * 100);
205
- // Newest history points first for the bar rendering logic (flex-direction: row-reverse)
206
  const historyBarPoints = relevantHistory.slice(-MAX_HISTORY_POINTS_FOR_DISPLAY).map(entry => entry.status).reverse();
207
-
208
  return { percentage: `${uptimePercent}%`, points: historyBarPoints };
209
  }
210
 
211
- // --- UI Rendering Logic ---
212
  function createUrlItemDOM(urlData) {
213
  const itemDiv = document.createElement('div');
214
  itemDiv.className = 'neumorphic-outset-sm p-4 mb-4 last:mb-0 url-item';
@@ -265,9 +299,9 @@
265
  }
266
 
267
  function renderUrlListUI() {
268
- urlList.innerHTML = ''; // Clear current list
269
  if (monitoredUrlsCache.length === 0) {
270
- urlList.innerHTML = `<p class="italic" style="color: var(--text-light-muted);">No URLs being monitored. Add one to begin.</p>`;
271
  return;
272
  }
273
  monitoredUrlsCache.forEach(urlData => {
@@ -276,27 +310,25 @@
276
  });
277
  }
278
 
279
- // --- Event Handlers ---
280
  async function handleAddUrl() {
281
  let urlToAdd = urlInput.value.trim();
282
- errorMsg.textContent = ''; // Clear previous errors
283
 
284
  if (!urlToAdd) {
285
  errorMsg.textContent = 'Please enter a URL.'; return;
286
  }
287
  if (!urlToAdd.startsWith('http://') && !urlToAdd.startsWith('https://')) {
288
- urlToAdd = 'https://' + urlToAdd; // Default to https
289
  }
290
  try {
291
- new URL(urlToAdd); // Basic client-side format validation
292
  } catch (_) {
293
  errorMsg.textContent = 'Invalid URL format.'; return;
294
  }
295
 
296
- // Optimistic client-side duplicate check (backend does final validation)
297
  const normalizedUrl = urlToAdd.replace(/\/+$/, '').toLowerCase();
298
  if (monitoredUrlsCache.some(u => u.url.replace(/\/+$/, '').toLowerCase() === normalizedUrl)) {
299
- errorMsg.textContent = 'This URL appears to be already monitored.'; return;
300
  }
301
 
302
  addUrlBtn.disabled = true; addUrlBtn.textContent = '...'; urlInput.disabled = true;
@@ -304,24 +336,21 @@
304
  try {
305
  await apiRequest('/api/urls', {
306
  method: 'POST',
307
- headers: { 'Content-Type': 'application/json' },
308
  body: JSON.stringify({ url: urlToAdd })
309
  });
310
- urlInput.value = ''; // Clear input on success
311
- await fetchAndRenderUrls(); // Refresh list to show the newly added URL
312
  } catch (e) {
313
- // apiRequest already displayed the error in errorMsg
314
  } finally {
315
  addUrlBtn.disabled = false; addUrlBtn.textContent = 'Add'; urlInput.disabled = false;
316
  }
317
  }
318
 
319
  async function handleRemoveUrl(urlIdToRemove) {
320
- // Optional: add a confirm() dialog here
321
- // if (!confirm(`Are you sure you want to remove this URL?`)) return;
322
  try {
323
  await apiRequest(`/api/urls/${urlIdToRemove}`, { method: 'DELETE' });
324
- // Remove from local cache and re-render for immediate UI update
325
  monitoredUrlsCache = monitoredUrlsCache.filter(url => url.id !== urlIdToRemove);
326
  renderUrlListUI();
327
  } catch (e) {
@@ -333,15 +362,15 @@
333
  setInterval(fetchAndRenderUrls, UI_REFRESH_INTERVAL_MS);
334
  }
335
 
336
- // --- Initialization ---
337
  addUrlBtn.addEventListener('click', handleAddUrl);
338
  urlInput.addEventListener('keypress', (event) => {
339
  if (event.key === 'Enter') { event.preventDefault(); handleAddUrl(); }
340
  });
341
 
342
  document.addEventListener('DOMContentLoaded', () => {
343
- fetchAndRenderUrls(); // Initial data load
344
- setupPeriodicDataRefresh(); // Start refreshing data from backend
 
345
  });
346
  </script>
347
  </body>
 
8
  <meta name="theme-color" content="#2c303a" media="(prefers-color-scheme: dark)">
9
  <script src="https://cdn.tailwindcss.com"></script>
10
  <style>
 
11
  :root {
12
  --light-bg: #e0e5ec; --dark-bg: #2c303a; --light-shadow-outer-1: #a3b1c6;
13
  --light-shadow-outer-2: #ffffff; --dark-shadow-outer-1: #22252e; --dark-shadow-outer-2: #363a46;
 
116
  const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
117
  const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
118
 
119
+ let monitoredUrlsCache = [];
120
+ const UI_REFRESH_INTERVAL_MS = 5000;
121
+ const HISTORY_DURATION_MS_FOR_DISPLAY = 60 * 60 * 1000;
 
122
  const MAX_HISTORY_POINTS_FOR_DISPLAY = 90;
123
+
124
+ const USER_ID_KEY = 'urlPingerUserId'; // Key for localStorage
125
+ let currentAppUserId = null; // In-memory cache for the user ID
126
+
127
+ // --- User ID Management ---
128
+ function getAppUserId() {
129
+ if (currentAppUserId) return currentAppUserId;
130
+
131
+ let userId = localStorage.getItem(USER_ID_KEY);
132
+ if (!userId) {
133
+ if (crypto.randomUUID) {
134
+ userId = crypto.randomUUID();
135
+ } else { // Fallback for older browsers (less robust UUID)
136
+ userId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
137
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
138
+ return v.toString(16);
139
+ });
140
+ }
141
+ localStorage.setItem(USER_ID_KEY, userId);
142
+ }
143
+ currentAppUserId = userId;
144
+ return userId;
145
+ }
146
 
147
+ // --- Theme Toggle ---
148
  function applyTheme(isDark) {
149
  if (isDark) {
150
  document.documentElement.classList.add('dark');
151
  themeToggleLightIcon.classList.remove('hidden');
152
  themeToggleDarkIcon.classList.add('hidden');
 
 
 
 
153
  } else {
154
  document.documentElement.classList.remove('dark');
155
  themeToggleLightIcon.classList.add('hidden');
156
  themeToggleDarkIcon.classList.remove('hidden');
 
 
 
 
157
  }
158
+ // Update meta theme-color and body background after class change
159
  requestAnimationFrame(() => {
160
+ const currentBg = getComputedStyle(document.documentElement).getPropertyValue('--light-bg').trim();
161
+ document.querySelector('meta[name="theme-color"]:not([media])').setAttribute('content', currentBg);
162
+ document.querySelector('meta[name="theme-color"][media="(prefers-color-scheme: dark)"]').setAttribute('content', currentBg); // Also set dark for consistency if needed
163
+ document.body.style.backgroundColor = currentBg;
164
  });
165
  }
166
  function toggleTheme() {
 
179
 
180
  // --- API Communication Wrapper ---
181
  async function apiRequest(endpoint, options = {}) {
182
+ const userId = getAppUserId(); // Get or generate user ID for each request
183
+
184
+ const defaultHeaders = { 'X-User-ID': userId };
185
+ // Only set Content-Type if body exists, otherwise let browser handle or set explicitly
186
+ if (options.body && typeof options.body === 'string' && (!options.headers || !options.headers['Content-Type'])) {
187
+ defaultHeaders['Content-Type'] = 'application/json';
188
+ }
189
+
190
+ const mergedOptions = {
191
+ ...options,
192
+ headers: {
193
+ ...defaultHeaders,
194
+ ...(options.headers || {}),
195
+ }
196
+ };
197
+
198
+ // For GET/HEAD/DELETE (typically no body), ensure Content-Type is not forced if not needed
199
+ const httpMethod = (mergedOptions.method || 'GET').toUpperCase();
200
+ if (!mergedOptions.body && (httpMethod === 'GET' || httpMethod === 'HEAD' || httpMethod === 'DELETE')) {
201
+ if (mergedOptions.headers && mergedOptions.headers['Content-Type'] === 'application/json') { // Only remove if it was our default
202
+ delete mergedOptions.headers['Content-Type'];
203
+ }
204
+ }
205
+
206
  try {
207
+ const response = await fetch(endpoint, mergedOptions);
208
  if (!response.ok) {
209
  const errorData = await response.json().catch(() => ({ message: response.statusText }));
210
  throw new Error(errorData.error || errorData.message || `HTTP Error ${response.status}`);
211
  }
212
  if (response.status === 204 || response.headers.get("content-length") === "0") {
213
+ return null;
214
  }
215
  return await response.json();
216
  } catch (error) {
217
  console.error(`API Request Error for ${endpoint}:`, error);
218
+ errorMsg.textContent = `Error: ${error.message}`;
219
+ throw error;
220
  }
221
  }
222
 
223
  async function fetchAndRenderUrls() {
224
  try {
225
+ const dataFromServer = await apiRequest('/api/urls'); // User ID header is added by apiRequest
226
+ monitoredUrlsCache = dataFromServer || [];
227
  renderUrlListUI();
228
  } catch (e) {
229
+ urlList.innerHTML = `<p class="italic" style="color: var(--text-light-muted);">Could not load URLs. Server may be down or user ID missing.</p>`;
 
 
230
  }
231
  }
232
 
 
233
  function getDisplayMetrics(backendHistoryArray) {
234
+ const historyForDisplay = (backendHistoryArray || []).map(h => ({ ...h, timestamp: h.timestamp * 1000 }));
 
235
  const cutoffTimeMs = Date.now() - HISTORY_DURATION_MS_FOR_DISPLAY;
236
  const relevantHistory = historyForDisplay.filter(entry => entry.timestamp >= cutoffTimeMs);
237
 
 
239
 
240
  const okCount = relevantHistory.filter(entry => entry.status === 'ok').length;
241
  const uptimePercent = Math.round((okCount / relevantHistory.length) * 100);
 
242
  const historyBarPoints = relevantHistory.slice(-MAX_HISTORY_POINTS_FOR_DISPLAY).map(entry => entry.status).reverse();
 
243
  return { percentage: `${uptimePercent}%`, points: historyBarPoints };
244
  }
245
 
 
246
  function createUrlItemDOM(urlData) {
247
  const itemDiv = document.createElement('div');
248
  itemDiv.className = 'neumorphic-outset-sm p-4 mb-4 last:mb-0 url-item';
 
299
  }
300
 
301
  function renderUrlListUI() {
302
+ urlList.innerHTML = '';
303
  if (monitoredUrlsCache.length === 0) {
304
+ urlList.innerHTML = `<p class="italic" style="color: var(--text-light-muted);">No URLs being monitored by you. Add one to begin.</p>`;
305
  return;
306
  }
307
  monitoredUrlsCache.forEach(urlData => {
 
310
  });
311
  }
312
 
 
313
  async function handleAddUrl() {
314
  let urlToAdd = urlInput.value.trim();
315
+ errorMsg.textContent = '';
316
 
317
  if (!urlToAdd) {
318
  errorMsg.textContent = 'Please enter a URL.'; return;
319
  }
320
  if (!urlToAdd.startsWith('http://') && !urlToAdd.startsWith('https://')) {
321
+ urlToAdd = 'https://' + urlToAdd;
322
  }
323
  try {
324
+ new URL(urlToAdd);
325
  } catch (_) {
326
  errorMsg.textContent = 'Invalid URL format.'; return;
327
  }
328
 
 
329
  const normalizedUrl = urlToAdd.replace(/\/+$/, '').toLowerCase();
330
  if (monitoredUrlsCache.some(u => u.url.replace(/\/+$/, '').toLowerCase() === normalizedUrl)) {
331
+ errorMsg.textContent = 'This URL appears to be already monitored by you.'; return;
332
  }
333
 
334
  addUrlBtn.disabled = true; addUrlBtn.textContent = '...'; urlInput.disabled = true;
 
336
  try {
337
  await apiRequest('/api/urls', {
338
  method: 'POST',
339
+ headers: { 'Content-Type': 'application/json' }, // Explicitly set for POST with JSON body
340
  body: JSON.stringify({ url: urlToAdd })
341
  });
342
+ urlInput.value = '';
343
+ await fetchAndRenderUrls();
344
  } catch (e) {
345
+ // apiRequest already displayed the error
346
  } finally {
347
  addUrlBtn.disabled = false; addUrlBtn.textContent = 'Add'; urlInput.disabled = false;
348
  }
349
  }
350
 
351
  async function handleRemoveUrl(urlIdToRemove) {
 
 
352
  try {
353
  await apiRequest(`/api/urls/${urlIdToRemove}`, { method: 'DELETE' });
 
354
  monitoredUrlsCache = monitoredUrlsCache.filter(url => url.id !== urlIdToRemove);
355
  renderUrlListUI();
356
  } catch (e) {
 
362
  setInterval(fetchAndRenderUrls, UI_REFRESH_INTERVAL_MS);
363
  }
364
 
 
365
  addUrlBtn.addEventListener('click', handleAddUrl);
366
  urlInput.addEventListener('keypress', (event) => {
367
  if (event.key === 'Enter') { event.preventDefault(); handleAddUrl(); }
368
  });
369
 
370
  document.addEventListener('DOMContentLoaded', () => {
371
+ getAppUserId(); // Ensure user ID is generated or loaded on start
372
+ fetchAndRenderUrls();
373
+ setupPeriodicDataRefresh();
374
  });
375
  </script>
376
  </body>