Codex commited on
Commit
4fc5091
·
1 Parent(s): 4992155

Update: Timeout 5m, Interval 15m, Basic Auth (Klaster1!)

Browse files
Files changed (1) hide show
  1. server.js +428 -411
server.js CHANGED
@@ -10,6 +10,23 @@ const port = process.env.PORT || 3000;
10
  app.use(express.json());
11
  app.use(express.urlencoded({ extended: true }));
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data');
14
  const WEBSITES_FILE = path.join(DATA_DIR, 'websites.json');
15
  let storageMode = process.env.DATABASE_URL ? 'db' : 'file';
@@ -24,7 +41,7 @@ const pool = storageMode === 'db'
24
  let websites = [];
25
 
26
  let statuses = {};
27
- const CHECK_INTERVAL = 60000; // 1 minute
28
  const SMTP_HOST = process.env.SMTP_HOST || 'smtp-relay.brevo.com';
29
  const SMTP_PORT = parseInt(process.env.SMTP_PORT || '587', 10);
30
  const SMTP_SECURE = process.env.SMTP_SECURE === 'true' || SMTP_PORT === 465;
@@ -395,10 +412,10 @@ async function loadWebsites() {
395
  async function checkWebsite(site) {
396
  try {
397
  const start = Date.now();
398
- const response = await axios.get(site.url, { timeout: 30000 });
399
  const duration = Date.now() - start;
400
  const newStatus = response.status === 200 ? 'UP' : 'DOWN';
401
-
402
  updateStatus(site, newStatus, response.status, duration, null);
403
  } catch (error) {
404
  const status = error.code === 'ECONNABORTED' ? 'TIMEOUT' : 'DOWN';
@@ -410,17 +427,17 @@ async function checkWebsite(site) {
410
  function updateStatus(site, newStatus, statusCode, duration, errorMsg) {
411
  const now = new Date();
412
  const prevStatus = statuses[site.url];
413
-
414
- let statusChangedAt = now;
415
- if (prevStatus && prevStatus.status === newStatus) {
416
- statusChangedAt = prevStatus.statusChangedAt;
417
- }
418
-
419
- statuses[site.url] = {
420
- name: site.name,
421
- url: site.url,
422
- status: newStatus,
423
- statusCode: statusCode,
424
  responseTime: duration,
425
  lastChecked: now,
426
  statusChangedAt: statusChangedAt,
@@ -434,23 +451,23 @@ function updateStatus(site, newStatus, statusCode, duration, errorMsg) {
434
  });
435
  }
436
  }
437
-
438
  async function checkAllWebsites() {
439
  console.log('Checking websites...');
440
  await loadWebsites();
441
  await Promise.all(websites.map(site => checkWebsite(site)));
442
  console.log('Check complete.');
443
  }
444
-
445
  // API endpoint to add a new website
446
  app.post('/api/sites', async (req, res) => {
447
  const { url } = req.body;
448
  if (!url) {
449
  return res.status(400).json({ error: 'URL is required' });
450
- }
451
-
452
- // Basic URL validation
453
- let validUrl = url;
454
  if (!validUrl.startsWith('http')) {
455
  validUrl = 'https://' + validUrl;
456
  }
@@ -476,7 +493,7 @@ app.post('/api/sites', async (req, res) => {
476
 
477
  // Initial check for the new site
478
  checkWebsite(newSite);
479
-
480
  res.json({ success: true, site: newSite });
481
  } catch (err) {
482
  console.error('Error saving website:', err);
@@ -563,66 +580,66 @@ async function start() {
563
  }
564
 
565
  start();
566
-
567
- function formatDuration(ms) {
568
- const seconds = Math.floor((ms / 1000) % 60);
569
- const minutes = Math.floor((ms / (1000 * 60)) % 60);
570
- const hours = Math.floor((ms / (1000 * 60 * 60)) % 24);
571
- const days = Math.floor(ms / (1000 * 60 * 60 * 24));
572
-
573
- const parts = [];
574
- if (days > 0) parts.push(`${days}d`);
575
- if (hours > 0) parts.push(`${hours}h`);
576
- if (minutes > 0) parts.push(`${minutes} min`);
577
- if (seconds > 0) parts.push(`${seconds} sec`);
578
-
579
- return parts.length > 0 ? parts.join(', ') : '0 sec';
580
- }
581
-
582
- app.get('/', (req, res) => {
583
- let html = `
584
- <!DOCTYPE html>
585
- <html lang="pl">
586
- <head>
587
- <meta charset="UTF-8">
588
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
589
- <title>Monitor Stron</title>
590
- <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
591
- <style>
592
- :root {
593
- --bg-color: #1a1e23;
594
- --card-bg: #22262b;
595
- --text-primary: #ecf0f1;
596
- --text-secondary: #95a5a6;
597
- --success-color: #2ecc71;
598
- --danger-color: #e74c3c;
599
- --warning-color: #f1c40f;
600
- --border-color: #2c3e50;
601
- --input-bg: #2c3e50;
602
- }
603
- body {
604
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
605
- background-color: var(--bg-color);
606
- color: var(--text-primary);
607
- margin: 0;
608
- padding: 40px 20px;
609
- }
610
- .container { max-width: 900px; margin: 0 auto; }
611
-
612
- h1 { text-align: center; margin-bottom: 30px; font-weight: 300; letter-spacing: 1px; }
613
-
614
- .controls {
615
- display: flex;
616
- gap: 15px;
617
- margin-bottom: 20px;
618
- flex-wrap: wrap;
619
- }
620
-
621
- .search-box {
622
- flex: 1;
623
- position: relative;
624
- }
625
-
626
  .add-box {
627
  flex: 1;
628
  display: flex;
@@ -645,184 +662,184 @@ app.get('/', (req, res) => {
645
  .btn-status:hover {
646
  background-color: #2980b9;
647
  }
648
-
649
- input {
650
- width: 100%;
651
- padding: 12px 15px;
652
- border-radius: 8px;
653
- border: 1px solid var(--border-color);
654
- background-color: var(--input-bg);
655
- color: var(--text-primary);
656
- font-size: 14px;
657
- box-sizing: border-box;
658
- }
659
-
660
- input:focus {
661
- outline: none;
662
- border-color: var(--success-color);
663
- }
664
-
665
- .btn-add {
666
- padding: 0 20px;
667
- background-color: var(--success-color);
668
- color: white;
669
- border: none;
670
- border-radius: 8px;
671
- cursor: pointer;
672
- font-weight: 600;
673
- transition: background 0.2s;
674
- white-space: nowrap;
675
- }
676
-
677
- .btn-add:hover { background-color: #27ae60; }
678
-
679
- .monitor-list {
680
- display: flex;
681
- flex-direction: column;
682
- gap: 15px;
683
- }
684
-
685
- .monitor-card {
686
- background-color: var(--card-bg);
687
- border: 1px solid var(--border-color);
688
- border-radius: 8px;
689
- padding: 15px 20px;
690
- display: flex;
691
- align-items: center;
692
- justify-content: space-between;
693
- transition: transform 0.2s;
694
- }
695
-
696
- .monitor-card:hover {
697
- transform: translateY(-2px);
698
- border-color: #34495e;
699
- }
700
-
701
- .card-left {
702
- display: flex;
703
- align-items: center;
704
- gap: 15px;
705
- }
706
-
707
- .status-icon {
708
- font-size: 24px;
709
- }
710
- .status-up { color: var(--success-color); }
711
- .status-down { color: var(--danger-color); }
712
-
713
- .site-details {
714
- display: flex;
715
- flex-direction: column;
716
- }
717
-
718
- .site-name {
719
- font-size: 16px;
720
- font-weight: 600;
721
- color: var(--text-primary);
722
- text-decoration: none;
723
- margin-bottom: 4px;
724
- }
725
-
726
- .site-name:hover { text-decoration: underline; }
727
-
728
- .site-meta {
729
- font-size: 12px;
730
- color: var(--text-secondary);
731
- display: flex;
732
- align-items: center;
733
- gap: 8px;
734
- }
735
-
736
- .badge {
737
- background: #2c3e50;
738
- padding: 2px 6px;
739
- border-radius: 4px;
740
- font-weight: 600;
741
- font-size: 10px;
742
- letter-spacing: 0.5px;
743
- }
744
-
745
- .card-right {
746
- color: var(--text-secondary);
747
- font-size: 14px;
748
- }
749
-
750
- .refresh-btn {
751
- position: fixed;
752
- bottom: 30px;
753
- right: 30px;
754
- background: var(--success-color);
755
- color: white;
756
- border: none;
757
- border-radius: 50%;
758
- width: 50px;
759
- height: 50px;
760
- cursor: pointer;
761
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
762
- font-size: 20px;
763
- display: flex;
764
- align-items: center;
765
- justify-content: center;
766
- transition: background 0.3s;
767
- }
768
- .refresh-btn:hover { background-color: #27ae60; }
769
-
770
- /* Dropdown Menu Styles */
771
- .menu-container {
772
- position: relative;
773
- display: inline-block;
774
- }
775
-
776
- .menu-btn {
777
- background: none;
778
- border: none;
779
- color: var(--text-secondary);
780
- cursor: pointer;
781
- padding: 5px;
782
- font-size: 16px;
783
- }
784
-
785
- .menu-btn:hover { color: var(--text-primary); }
786
-
787
- .dropdown-content {
788
- display: none;
789
- position: absolute;
790
- right: 0;
791
- top: 100%;
792
- background-color: var(--card-bg);
793
- min-width: 120px;
794
- box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
795
- z-index: 1;
796
- border: 1px solid var(--border-color);
797
- border-radius: 4px;
798
- }
799
-
800
- .dropdown-content button {
801
- color: var(--text-primary);
802
- padding: 10px 16px;
803
- text-decoration: none;
804
- display: block;
805
- width: 100%;
806
- text-align: left;
807
- background: none;
808
- border: none;
809
- cursor: pointer;
810
- font-size: 14px;
811
- }
812
-
813
- .dropdown-content button:hover {
814
- background-color: #34495e;
815
- }
816
-
817
- .show-menu { display: block; }
818
-
819
- </style>
820
- <meta http-equiv="refresh" content="30">
821
- </head>
822
- <body>
823
- <div class="container">
824
- <h1>Monitor Statusu Stron (${websites.length})</h1>
825
-
826
  <div class="controls">
827
  <div class="search-box">
828
  <input type="text" id="searchInput" placeholder="Szukaj domeny...">
@@ -833,163 +850,163 @@ app.get('/', (req, res) => {
833
  </div>
834
  <button class="btn-status" onclick="sendStatusEmail()">Wyślij status mailem</button>
835
  </div>
836
-
837
- <div class="monitor-list" id="monitorList">
838
- `;
839
-
840
- // Reverse websites to show newest first, or keep order? Let's keep order from JSON.
841
- for (const site of websites) {
842
- const status = statuses[site.url];
843
- let cardHtml = '';
844
-
845
- if (!status) {
846
- // Pending state
847
- cardHtml = `
848
- <div class="monitor-card" data-name="${site.name}">
849
- <div class="card-left">
850
- <i class="fas fa-circle-notch fa-spin status-icon" style="color: #95a5a6;"></i>
851
- <div class="site-details">
852
- <a href="${site.url}" target="_blank" class="site-name">${site.name}</a>
853
- <div class="site-meta">
854
- <span class="badge">HTTP</span>
855
- <span>Oczekiwanie...</span>
856
- </div>
857
- </div>
858
- </div>
859
- <div class="card-right">
860
- <div class="menu-container">
861
- <button class="menu-btn" onclick="toggleMenu('${site.url}')"><i class="fas fa-ellipsis-h"></i></button>
862
- <div id="menu-${btoa(site.url)}" class="dropdown-content">
863
- <button onclick="deleteWebsite('${site.url}')"><i class="fas fa-trash-alt" style="margin-right:8px;color:#e74c3c"></i> Usuń</button>
864
- </div>
865
- </div>
866
- </div>
867
- </div>`;
868
- } else {
869
- const isUp = status.status === 'UP';
870
- const iconClass = isUp ? 'fa-arrow-circle-up status-up' : 'fa-exclamation-circle status-down';
871
- const statusLabel = isUp ? 'Up' : (status.error ? 'Down' : 'Issue');
872
- const duration = formatDuration(new Date() - new Date(status.statusChangedAt));
873
-
874
- cardHtml = `
875
- <div class="monitor-card" data-name="${status.name}">
876
- <div class="card-left">
877
- <i class="fas ${iconClass} status-icon"></i>
878
- <div class="site-details">
879
- <a href="${status.url}" target="_blank" class="site-name">${status.name}</a>
880
- <div class="site-meta">
881
- <span class="badge">HTTP</span>
882
- <span style="color: ${isUp ? '#2ecc71' : '#e74c3c'}">${statusLabel}</span>
883
- <span>${duration}</span>
884
- </div>
885
- </div>
886
- </div>
887
- <div class="card-right">
888
- <span title="Response Time">${status.responseTime}ms</span>
889
- <span style="margin: 0 10px; color: #34495e;">|</span>
890
- <div class="menu-container">
891
- <button class="menu-btn" onclick="toggleMenu('${status.url}')"><i class="fas fa-ellipsis-h"></i></button>
892
- <div id="menu-${btoa(status.url)}" class="dropdown-content">
893
- <button onclick="deleteWebsite('${status.url}')"><i class="fas fa-trash-alt" style="margin-right:8px;color:#e74c3c"></i> Usuń</button>
894
- </div>
895
- </div>
896
- </div>
897
- </div>
898
- `;
899
- }
900
- html += cardHtml;
901
- }
902
-
903
- html += `
904
- </div>
905
- </div>
906
- <button class="refresh-btn" onclick="location.reload()"><i class="fas fa-sync-alt"></i></button>
907
-
908
- <script>
909
- // Search Functionality
910
- document.getElementById('searchInput').addEventListener('keyup', function() {
911
- const searchValue = this.value.toLowerCase();
912
- const cards = document.querySelectorAll('.monitor-card');
913
-
914
- cards.forEach(card => {
915
- const siteName = card.getAttribute('data-name').toLowerCase();
916
- if (siteName.includes(searchValue)) {
917
- card.style.display = 'flex';
918
- } else {
919
- card.style.display = 'none';
920
- }
921
- });
922
- });
923
-
924
- // Add Website Functionality
925
- async function addWebsite() {
926
- const urlInput = document.getElementById('addUrlInput');
927
- const url = urlInput.value.trim();
928
-
929
- if (!url) return alert('Wprowadź URL');
930
-
931
- try {
932
- const response = await fetch('/api/sites', {
933
- method: 'POST',
934
- headers: { 'Content-Type': 'application/json' },
935
- body: JSON.stringify({ url })
936
- });
937
-
938
- const data = await response.json();
939
-
940
- if (response.ok) {
941
- urlInput.value = '';
942
- location.reload(); // Reload to show new site
943
- } else {
944
- alert(data.error || 'Błąd dodawania strony');
945
- }
946
- } catch (error) {
947
- console.error('Error:', error);
948
- alert('Wystąpił błąd podczas dodawania strony');
949
- }
950
- }
951
-
952
- // Menu Functionality
953
- function toggleMenu(url) {
954
- const menuId = 'menu-' + btoa(url);
955
- const menu = document.getElementById(menuId);
956
-
957
- // Close all other menus
958
- document.querySelectorAll('.dropdown-content').forEach(m => {
959
- if (m.id !== menuId) m.classList.remove('show-menu');
960
- });
961
-
962
- menu.classList.toggle('show-menu');
963
- }
964
-
965
- // Close menu when clicking outside
966
- window.onclick = function(event) {
967
- if (!event.target.matches('.menu-btn') && !event.target.matches('.menu-btn i')) {
968
- document.querySelectorAll('.dropdown-content').forEach(m => {
969
- m.classList.remove('show-menu');
970
- });
971
- }
972
- }
973
-
974
  // Delete Website Functionality
975
  async function deleteWebsite(url) {
976
  if (!confirm('Czy na pewno chcesz usunąć tę stronę z monitorowania?')) return;
977
 
978
  try {
979
- const response = await fetch('/api/sites', {
980
- method: 'DELETE',
981
- headers: { 'Content-Type': 'application/json' },
982
- body: JSON.stringify({ url })
983
- });
984
-
985
- const data = await response.json();
986
-
987
- if (response.ok) {
988
- location.reload(); // Reload to update list
989
- } else {
990
- alert(data.error || 'Błąd usuwania strony');
991
- }
992
- } catch (error) {
993
  console.error('Error:', error);
994
  alert('Wystąpił błąd podczas usuwania strony');
995
  }
 
10
  app.use(express.json());
11
  app.use(express.urlencoded({ extended: true }));
12
 
13
+ // Basic Auth
14
+ app.use((req, res, next) => {
15
+ // Default user 'admin' since only password was specified
16
+ const user = 'admin';
17
+ const pass = 'Klaster1!';
18
+
19
+ const b64auth = (req.headers.authorization || '').split(' ')[1] || '';
20
+ const [login, password] = Buffer.from(b64auth, 'base64').toString().split(':');
21
+
22
+ if (login && password && login === user && password === pass) {
23
+ return next();
24
+ }
25
+
26
+ res.set('WWW-Authenticate', 'Basic realm="Monitor Stron"');
27
+ res.status(401).send('Dostęp zabroniony. Wymagane logowanie.');
28
+ });
29
+
30
  const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data');
31
  const WEBSITES_FILE = path.join(DATA_DIR, 'websites.json');
32
  let storageMode = process.env.DATABASE_URL ? 'db' : 'file';
 
41
  let websites = [];
42
 
43
  let statuses = {};
44
+ const CHECK_INTERVAL = 900000; // 15 minutes
45
  const SMTP_HOST = process.env.SMTP_HOST || 'smtp-relay.brevo.com';
46
  const SMTP_PORT = parseInt(process.env.SMTP_PORT || '587', 10);
47
  const SMTP_SECURE = process.env.SMTP_SECURE === 'true' || SMTP_PORT === 465;
 
412
  async function checkWebsite(site) {
413
  try {
414
  const start = Date.now();
415
+ const response = await axios.get(site.url, { timeout: 300000 }); // 5 minutes
416
  const duration = Date.now() - start;
417
  const newStatus = response.status === 200 ? 'UP' : 'DOWN';
418
+
419
  updateStatus(site, newStatus, response.status, duration, null);
420
  } catch (error) {
421
  const status = error.code === 'ECONNABORTED' ? 'TIMEOUT' : 'DOWN';
 
427
  function updateStatus(site, newStatus, statusCode, duration, errorMsg) {
428
  const now = new Date();
429
  const prevStatus = statuses[site.url];
430
+
431
+ let statusChangedAt = now;
432
+ if (prevStatus && prevStatus.status === newStatus) {
433
+ statusChangedAt = prevStatus.statusChangedAt;
434
+ }
435
+
436
+ statuses[site.url] = {
437
+ name: site.name,
438
+ url: site.url,
439
+ status: newStatus,
440
+ statusCode: statusCode,
441
  responseTime: duration,
442
  lastChecked: now,
443
  statusChangedAt: statusChangedAt,
 
451
  });
452
  }
453
  }
454
+
455
  async function checkAllWebsites() {
456
  console.log('Checking websites...');
457
  await loadWebsites();
458
  await Promise.all(websites.map(site => checkWebsite(site)));
459
  console.log('Check complete.');
460
  }
461
+
462
  // API endpoint to add a new website
463
  app.post('/api/sites', async (req, res) => {
464
  const { url } = req.body;
465
  if (!url) {
466
  return res.status(400).json({ error: 'URL is required' });
467
+ }
468
+
469
+ // Basic URL validation
470
+ let validUrl = url;
471
  if (!validUrl.startsWith('http')) {
472
  validUrl = 'https://' + validUrl;
473
  }
 
493
 
494
  // Initial check for the new site
495
  checkWebsite(newSite);
496
+
497
  res.json({ success: true, site: newSite });
498
  } catch (err) {
499
  console.error('Error saving website:', err);
 
580
  }
581
 
582
  start();
583
+
584
+ function formatDuration(ms) {
585
+ const seconds = Math.floor((ms / 1000) % 60);
586
+ const minutes = Math.floor((ms / (1000 * 60)) % 60);
587
+ const hours = Math.floor((ms / (1000 * 60 * 60)) % 24);
588
+ const days = Math.floor(ms / (1000 * 60 * 60 * 24));
589
+
590
+ const parts = [];
591
+ if (days > 0) parts.push(`${days}d`);
592
+ if (hours > 0) parts.push(`${hours}h`);
593
+ if (minutes > 0) parts.push(`${minutes} min`);
594
+ if (seconds > 0) parts.push(`${seconds} sec`);
595
+
596
+ return parts.length > 0 ? parts.join(', ') : '0 sec';
597
+ }
598
+
599
+ app.get('/', (req, res) => {
600
+ let html = `
601
+ <!DOCTYPE html>
602
+ <html lang="pl">
603
+ <head>
604
+ <meta charset="UTF-8">
605
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
606
+ <title>Monitor Stron</title>
607
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
608
+ <style>
609
+ :root {
610
+ --bg-color: #1a1e23;
611
+ --card-bg: #22262b;
612
+ --text-primary: #ecf0f1;
613
+ --text-secondary: #95a5a6;
614
+ --success-color: #2ecc71;
615
+ --danger-color: #e74c3c;
616
+ --warning-color: #f1c40f;
617
+ --border-color: #2c3e50;
618
+ --input-bg: #2c3e50;
619
+ }
620
+ body {
621
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
622
+ background-color: var(--bg-color);
623
+ color: var(--text-primary);
624
+ margin: 0;
625
+ padding: 40px 20px;
626
+ }
627
+ .container { max-width: 900px; margin: 0 auto; }
628
+
629
+ h1 { text-align: center; margin-bottom: 30px; font-weight: 300; letter-spacing: 1px; }
630
+
631
+ .controls {
632
+ display: flex;
633
+ gap: 15px;
634
+ margin-bottom: 20px;
635
+ flex-wrap: wrap;
636
+ }
637
+
638
+ .search-box {
639
+ flex: 1;
640
+ position: relative;
641
+ }
642
+
643
  .add-box {
644
  flex: 1;
645
  display: flex;
 
662
  .btn-status:hover {
663
  background-color: #2980b9;
664
  }
665
+
666
+ input {
667
+ width: 100%;
668
+ padding: 12px 15px;
669
+ border-radius: 8px;
670
+ border: 1px solid var(--border-color);
671
+ background-color: var(--input-bg);
672
+ color: var(--text-primary);
673
+ font-size: 14px;
674
+ box-sizing: border-box;
675
+ }
676
+
677
+ input:focus {
678
+ outline: none;
679
+ border-color: var(--success-color);
680
+ }
681
+
682
+ .btn-add {
683
+ padding: 0 20px;
684
+ background-color: var(--success-color);
685
+ color: white;
686
+ border: none;
687
+ border-radius: 8px;
688
+ cursor: pointer;
689
+ font-weight: 600;
690
+ transition: background 0.2s;
691
+ white-space: nowrap;
692
+ }
693
+
694
+ .btn-add:hover { background-color: #27ae60; }
695
+
696
+ .monitor-list {
697
+ display: flex;
698
+ flex-direction: column;
699
+ gap: 15px;
700
+ }
701
+
702
+ .monitor-card {
703
+ background-color: var(--card-bg);
704
+ border: 1px solid var(--border-color);
705
+ border-radius: 8px;
706
+ padding: 15px 20px;
707
+ display: flex;
708
+ align-items: center;
709
+ justify-content: space-between;
710
+ transition: transform 0.2s;
711
+ }
712
+
713
+ .monitor-card:hover {
714
+ transform: translateY(-2px);
715
+ border-color: #34495e;
716
+ }
717
+
718
+ .card-left {
719
+ display: flex;
720
+ align-items: center;
721
+ gap: 15px;
722
+ }
723
+
724
+ .status-icon {
725
+ font-size: 24px;
726
+ }
727
+ .status-up { color: var(--success-color); }
728
+ .status-down { color: var(--danger-color); }
729
+
730
+ .site-details {
731
+ display: flex;
732
+ flex-direction: column;
733
+ }
734
+
735
+ .site-name {
736
+ font-size: 16px;
737
+ font-weight: 600;
738
+ color: var(--text-primary);
739
+ text-decoration: none;
740
+ margin-bottom: 4px;
741
+ }
742
+
743
+ .site-name:hover { text-decoration: underline; }
744
+
745
+ .site-meta {
746
+ font-size: 12px;
747
+ color: var(--text-secondary);
748
+ display: flex;
749
+ align-items: center;
750
+ gap: 8px;
751
+ }
752
+
753
+ .badge {
754
+ background: #2c3e50;
755
+ padding: 2px 6px;
756
+ border-radius: 4px;
757
+ font-weight: 600;
758
+ font-size: 10px;
759
+ letter-spacing: 0.5px;
760
+ }
761
+
762
+ .card-right {
763
+ color: var(--text-secondary);
764
+ font-size: 14px;
765
+ }
766
+
767
+ .refresh-btn {
768
+ position: fixed;
769
+ bottom: 30px;
770
+ right: 30px;
771
+ background: var(--success-color);
772
+ color: white;
773
+ border: none;
774
+ border-radius: 50%;
775
+ width: 50px;
776
+ height: 50px;
777
+ cursor: pointer;
778
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
779
+ font-size: 20px;
780
+ display: flex;
781
+ align-items: center;
782
+ justify-content: center;
783
+ transition: background 0.3s;
784
+ }
785
+ .refresh-btn:hover { background-color: #27ae60; }
786
+
787
+ /* Dropdown Menu Styles */
788
+ .menu-container {
789
+ position: relative;
790
+ display: inline-block;
791
+ }
792
+
793
+ .menu-btn {
794
+ background: none;
795
+ border: none;
796
+ color: var(--text-secondary);
797
+ cursor: pointer;
798
+ padding: 5px;
799
+ font-size: 16px;
800
+ }
801
+
802
+ .menu-btn:hover { color: var(--text-primary); }
803
+
804
+ .dropdown-content {
805
+ display: none;
806
+ position: absolute;
807
+ right: 0;
808
+ top: 100%;
809
+ background-color: var(--card-bg);
810
+ min-width: 120px;
811
+ box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
812
+ z-index: 1;
813
+ border: 1px solid var(--border-color);
814
+ border-radius: 4px;
815
+ }
816
+
817
+ .dropdown-content button {
818
+ color: var(--text-primary);
819
+ padding: 10px 16px;
820
+ text-decoration: none;
821
+ display: block;
822
+ width: 100%;
823
+ text-align: left;
824
+ background: none;
825
+ border: none;
826
+ cursor: pointer;
827
+ font-size: 14px;
828
+ }
829
+
830
+ .dropdown-content button:hover {
831
+ background-color: #34495e;
832
+ }
833
+
834
+ .show-menu { display: block; }
835
+
836
+ </style>
837
+ <meta http-equiv="refresh" content="30">
838
+ </head>
839
+ <body>
840
+ <div class="container">
841
+ <h1>Monitor Statusu Stron (${websites.length})</h1>
842
+
843
  <div class="controls">
844
  <div class="search-box">
845
  <input type="text" id="searchInput" placeholder="Szukaj domeny...">
 
850
  </div>
851
  <button class="btn-status" onclick="sendStatusEmail()">Wyślij status mailem</button>
852
  </div>
853
+
854
+ <div class="monitor-list" id="monitorList">
855
+ `;
856
+
857
+ // Reverse websites to show newest first, or keep order? Let's keep order from JSON.
858
+ for (const site of websites) {
859
+ const status = statuses[site.url];
860
+ let cardHtml = '';
861
+
862
+ if (!status) {
863
+ // Pending state
864
+ cardHtml = `
865
+ <div class="monitor-card" data-name="${site.name}">
866
+ <div class="card-left">
867
+ <i class="fas fa-circle-notch fa-spin status-icon" style="color: #95a5a6;"></i>
868
+ <div class="site-details">
869
+ <a href="${site.url}" target="_blank" class="site-name">${site.name}</a>
870
+ <div class="site-meta">
871
+ <span class="badge">HTTP</span>
872
+ <span>Oczekiwanie...</span>
873
+ </div>
874
+ </div>
875
+ </div>
876
+ <div class="card-right">
877
+ <div class="menu-container">
878
+ <button class="menu-btn" onclick="toggleMenu('${site.url}')"><i class="fas fa-ellipsis-h"></i></button>
879
+ <div id="menu-${btoa(site.url)}" class="dropdown-content">
880
+ <button onclick="deleteWebsite('${site.url}')"><i class="fas fa-trash-alt" style="margin-right:8px;color:#e74c3c"></i> Usuń</button>
881
+ </div>
882
+ </div>
883
+ </div>
884
+ </div>`;
885
+ } else {
886
+ const isUp = status.status === 'UP';
887
+ const iconClass = isUp ? 'fa-arrow-circle-up status-up' : 'fa-exclamation-circle status-down';
888
+ const statusLabel = isUp ? 'Up' : (status.error ? 'Down' : 'Issue');
889
+ const duration = formatDuration(new Date() - new Date(status.statusChangedAt));
890
+
891
+ cardHtml = `
892
+ <div class="monitor-card" data-name="${status.name}">
893
+ <div class="card-left">
894
+ <i class="fas ${iconClass} status-icon"></i>
895
+ <div class="site-details">
896
+ <a href="${status.url}" target="_blank" class="site-name">${status.name}</a>
897
+ <div class="site-meta">
898
+ <span class="badge">HTTP</span>
899
+ <span style="color: ${isUp ? '#2ecc71' : '#e74c3c'}">${statusLabel}</span>
900
+ <span>${duration}</span>
901
+ </div>
902
+ </div>
903
+ </div>
904
+ <div class="card-right">
905
+ <span title="Response Time">${status.responseTime}ms</span>
906
+ <span style="margin: 0 10px; color: #34495e;">|</span>
907
+ <div class="menu-container">
908
+ <button class="menu-btn" onclick="toggleMenu('${status.url}')"><i class="fas fa-ellipsis-h"></i></button>
909
+ <div id="menu-${btoa(status.url)}" class="dropdown-content">
910
+ <button onclick="deleteWebsite('${status.url}')"><i class="fas fa-trash-alt" style="margin-right:8px;color:#e74c3c"></i> Usuń</button>
911
+ </div>
912
+ </div>
913
+ </div>
914
+ </div>
915
+ `;
916
+ }
917
+ html += cardHtml;
918
+ }
919
+
920
+ html += `
921
+ </div>
922
+ </div>
923
+ <button class="refresh-btn" onclick="location.reload()"><i class="fas fa-sync-alt"></i></button>
924
+
925
+ <script>
926
+ // Search Functionality
927
+ document.getElementById('searchInput').addEventListener('keyup', function() {
928
+ const searchValue = this.value.toLowerCase();
929
+ const cards = document.querySelectorAll('.monitor-card');
930
+
931
+ cards.forEach(card => {
932
+ const siteName = card.getAttribute('data-name').toLowerCase();
933
+ if (siteName.includes(searchValue)) {
934
+ card.style.display = 'flex';
935
+ } else {
936
+ card.style.display = 'none';
937
+ }
938
+ });
939
+ });
940
+
941
+ // Add Website Functionality
942
+ async function addWebsite() {
943
+ const urlInput = document.getElementById('addUrlInput');
944
+ const url = urlInput.value.trim();
945
+
946
+ if (!url) return alert('Wprowadź URL');
947
+
948
+ try {
949
+ const response = await fetch('/api/sites', {
950
+ method: 'POST',
951
+ headers: { 'Content-Type': 'application/json' },
952
+ body: JSON.stringify({ url })
953
+ });
954
+
955
+ const data = await response.json();
956
+
957
+ if (response.ok) {
958
+ urlInput.value = '';
959
+ location.reload(); // Reload to show new site
960
+ } else {
961
+ alert(data.error || 'Błąd dodawania strony');
962
+ }
963
+ } catch (error) {
964
+ console.error('Error:', error);
965
+ alert('Wystąpił błąd podczas dodawania strony');
966
+ }
967
+ }
968
+
969
+ // Menu Functionality
970
+ function toggleMenu(url) {
971
+ const menuId = 'menu-' + btoa(url);
972
+ const menu = document.getElementById(menuId);
973
+
974
+ // Close all other menus
975
+ document.querySelectorAll('.dropdown-content').forEach(m => {
976
+ if (m.id !== menuId) m.classList.remove('show-menu');
977
+ });
978
+
979
+ menu.classList.toggle('show-menu');
980
+ }
981
+
982
+ // Close menu when clicking outside
983
+ window.onclick = function(event) {
984
+ if (!event.target.matches('.menu-btn') && !event.target.matches('.menu-btn i')) {
985
+ document.querySelectorAll('.dropdown-content').forEach(m => {
986
+ m.classList.remove('show-menu');
987
+ });
988
+ }
989
+ }
990
+
991
  // Delete Website Functionality
992
  async function deleteWebsite(url) {
993
  if (!confirm('Czy na pewno chcesz usunąć tę stronę z monitorowania?')) return;
994
 
995
  try {
996
+ const response = await fetch('/api/sites', {
997
+ method: 'DELETE',
998
+ headers: { 'Content-Type': 'application/json' },
999
+ body: JSON.stringify({ url })
1000
+ });
1001
+
1002
+ const data = await response.json();
1003
+
1004
+ if (response.ok) {
1005
+ location.reload(); // Reload to update list
1006
+ } else {
1007
+ alert(data.error || 'Błąd usuwania strony');
1008
+ }
1009
+ } catch (error) {
1010
  console.error('Error:', error);
1011
  alert('Wystąpił błąd podczas usuwania strony');
1012
  }