Alvin3y1 commited on
Commit
706ce69
·
verified ·
1 Parent(s): 14e0dfb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +69 -236
app.py CHANGED
@@ -3,15 +3,15 @@ import time
3
  import requests
4
  import os
5
  import datetime
6
- from flask import Flask, request, jsonify
 
7
 
8
  app = Flask(__name__)
9
 
10
  # --- CONFIGURATION ---
11
  URL_FILE = 'urls.txt'
12
  SELF_PING_URL = "https://alvin3y1-ping.hf.space/"
13
- INTERVAL_HOURS = 4
14
- INTERVAL_SECONDS = INTERVAL_HOURS * 3600
15
 
16
  # --- GLOBAL STATE ---
17
  monitoring_state = {}
@@ -19,304 +19,137 @@ scan_state = {
19
  'is_scanning': False,
20
  'total': 0,
21
  'completed': 0,
22
- 'last_scan_time': 'Waiting for initial scan...'
23
  }
24
  state_lock = threading.Lock()
25
 
26
- # --- HTML / FRONTEND ---
27
- # Pure static HTML using Tailwind CSS for a premium, minimal UI.
28
  HTML_TEMPLATE = """
29
  <!DOCTYPE html>
30
  <html lang="en" class="dark">
31
  <head>
32
  <meta charset="UTF-8">
33
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
34
- <title>Status | Alvin</title>
35
  <script src="https://cdn.tailwindcss.com"></script>
36
- <script>
37
- tailwind.config = {
38
- darkMode: 'class',
39
- theme: {
40
- extend: {
41
- colors: { bgMain: '#0a0a0a', card: '#111111', borderSubtle: '#222222' },
42
- fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'], mono: ['ui-monospace', 'monospace'] }
43
- }
44
- }
45
- }
46
- </script>
47
- <style>
48
- body { background-color: #0a0a0a; color: #ededed; }
49
- .fade-in { animation: fadeIn 0.4s ease-in-out; }
50
- @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
51
- /* Smooth progress bar transition */
52
- #progress-fill { transition: width 0.3s ease-in-out; }
53
- </style>
54
  </head>
55
- <body class="font-sans antialiased min-h-screen flex flex-col items-center py-12 px-4 sm:px-6">
56
-
57
- <div class="w-full max-w-3xl">
58
- <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 border-b border-borderSubtle pb-6">
59
- <div>
60
- <h1 class="text-2xl font-semibold tracking-tight">System Status</h1>
61
- <p id="last-updated" class="text-sm text-gray-500 mt-1">Fetching latest data...</p>
62
- </div>
63
- <button onclick="triggerScan()" id="scan-btn" class="mt-4 sm:mt-0 px-4 py-2 bg-white text-black text-sm font-medium rounded-md hover:bg-gray-200 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-white focus:ring-offset-bgMain">
64
- Run Diagnostics
65
- </button>
66
  </div>
67
-
68
- <div id="progress-container" class="hidden mb-8 fade-in">
69
- <div class="flex justify-between text-xs text-gray-400 mb-2">
70
- <span id="progress-text">Scanning...</span>
71
- <span id="progress-percent">0%</span>
72
- </div>
73
- <div class="w-full h-1 bg-borderSubtle rounded-full overflow-hidden">
74
- <div id="progress-fill" class="h-full bg-blue-500 w-0"></div>
75
  </div>
76
  </div>
77
 
78
- <div id="status-container" class="space-y-4">
79
- </div>
80
  </div>
81
 
82
  <script>
83
- let isScanning = false;
84
-
85
  async function fetchStatus() {
86
- try {
87
- const res = await fetch('/api/status');
88
- const data = await res.json();
89
- updateUI(data);
90
- } catch (err) {
91
- console.error("Failed to fetch status", err);
92
- }
93
- }
94
-
95
- async function triggerScan() {
96
- if (isScanning) return;
97
- document.getElementById('scan-btn').disabled = true;
98
- document.getElementById('scan-btn').innerText = 'Starting...';
99
- await fetch('/api/scan', { method: 'POST' });
100
- fetchStatus(); // immediate update
101
- }
102
-
103
- async function retryUrl(url) {
104
- await fetch('/api/retry', {
105
- method: 'POST',
106
- headers: { 'Content-Type': 'application/json' },
107
- body: JSON.stringify({ url: url })
108
- });
109
- fetchStatus(); // immediate update
110
- }
111
-
112
- function updateUI(data) {
113
- // 1. Update Scan State
114
- const scanBtn = document.getElementById('scan-btn');
115
- const progressContainer = document.getElementById('progress-container');
116
- const progressFill = document.getElementById('progress-fill');
117
- const progressText = document.getElementById('progress-text');
118
- const progressPercent = document.getElementById('progress-percent');
119
-
120
- isScanning = data.scan.is_scanning;
121
 
122
- if (isScanning) {
123
- scanBtn.disabled = true;
124
- scanBtn.innerText = 'Scanning...';
125
- progressContainer.classList.remove('hidden');
126
-
127
- let percent = 0;
128
- if (data.scan.total > 0) {
129
- percent = Math.round((data.scan.completed / data.scan.total) * 100);
130
- }
131
- progressFill.style.width = percent + '%';
132
- progressText.innerText = `Checking ${data.scan.completed} of ${data.scan.total} services...`;
133
- progressPercent.innerText = percent + '%';
134
  } else {
135
- scanBtn.disabled = false;
136
- scanBtn.innerText = 'Run Diagnostics';
137
- progressContainer.classList.add('hidden');
138
  }
139
 
140
- // 2. Update Header Time
141
- document.getElementById('last-updated').innerText = `Last cycle: ${data.scan.last_scan_time}`;
142
-
143
- // 3. Render Status List (Showing ONLY DOWN urls)
144
- const container = document.getElementById('status-container');
145
- const downUrls = data.down_urls;
146
-
147
- if (downUrls.length === 0) {
148
- // All Operational State
149
- container.innerHTML = `
150
- <div class="flex items-center justify-center py-16 bg-card border border-borderSubtle rounded-xl fade-in">
151
- <div class="flex flex-col items-center">
152
- <div class="h-3 w-3 bg-green-500 rounded-full shadow-[0_0_15px_rgba(34,197,94,0.5)] mb-4 animate-pulse"></div>
153
- <h2 class="text-lg font-medium text-gray-200">All Systems Operational</h2>
154
- <p class="text-sm text-gray-500 mt-1">Monitoring ${data.total_monitored} endpoints</p>
155
- </div>
156
- </div>
157
- `;
158
- } else {
159
- // Down URLs State
160
- let html = `
161
- <div class="mb-4 flex items-center gap-2">
162
- <div class="h-2 w-2 bg-red-500 rounded-full shadow-[0_0_10px_rgba(239,68,68,0.8)]"></div>
163
- <h2 class="text-sm font-medium text-red-400 uppercase tracking-widest">${downUrls.length} Service(s) Down</h2>
164
- </div>
165
- `;
166
-
167
- downUrls.forEach(item => {
168
- html += `
169
- <div class="bg-card border border-borderSubtle rounded-xl p-5 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 fade-in">
170
- <div class="overflow-hidden">
171
- <a href="${item.url}" target="_blank" class="text-white font-mono text-sm hover:underline truncate block w-full max-w-md">
172
- ${item.url}
173
- </a>
174
- <div class="flex items-center gap-3 mt-2">
175
- <span class="inline-flex items-center rounded-md bg-red-400/10 px-2 py-1 text-xs font-medium text-red-400 ring-1 ring-inset ring-red-400/20">
176
- ERR ${item.code}
177
- </span>
178
- <span class="text-xs text-gray-500">${item.msg}</span>
179
- </div>
180
- </div>
181
- <button onclick="retryUrl('${item.url}')" class="px-3 py-1.5 bg-borderSubtle hover:bg-gray-700 text-xs font-medium rounded-md transition-colors whitespace-nowrap">
182
- Retry
183
- </button>
184
  </div>
185
- `;
186
- });
187
- container.innerHTML = html;
188
- }
 
 
 
 
 
 
 
189
  }
190
-
191
- // Poll API every 2 seconds for real-time updates
192
  setInterval(fetchStatus, 2000);
193
-
194
- // Initial fetch on load
195
  fetchStatus();
196
  </script>
197
  </body>
198
  </html>
199
  """
200
 
 
201
  def get_all_urls():
202
- """Reads URLs from file and appends the hardwired self-ping URL."""
203
- urls = []
204
  if os.path.exists(URL_FILE):
205
  with open(URL_FILE, 'r') as f:
206
- urls = [line.strip() for line in f if line.strip()]
207
-
208
- if SELF_PING_URL not in urls:
209
- urls.append(SELF_PING_URL)
210
-
211
- with state_lock:
212
- for url in urls:
213
- if url not in monitoring_state:
214
- monitoring_state[url] = {
215
- 'status': 'PENDING',
216
- 'code': '-',
217
- 'time': '-',
218
- 'msg': 'Waiting for check...'
219
- }
220
- return urls
221
 
222
  def check_single_url(url):
223
- """Pings a single URL and updates the global state."""
224
- timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
225
- result = {}
226
-
227
  try:
228
- response = requests.get(url, timeout=10)
229
- if response.status_code >= 400:
230
- result = {'status': 'DOWN', 'code': response.status_code, 'time': timestamp, 'msg': "HTTP Error"}
231
- else:
232
- result = {'status': 'UP', 'code': response.status_code, 'time': timestamp, 'msg': "OK"}
233
- except requests.exceptions.Timeout:
234
- result = {'status': 'DOWN', 'code': 'TIMEOUT', 'time': timestamp, 'msg': "Request Timed Out"}
235
- except requests.exceptions.ConnectionError:
236
- result = {'status': 'DOWN', 'code': 'ERR', 'time': timestamp, 'msg': "Connection Failed"}
237
  except Exception as e:
238
- result = {'status': 'DOWN', 'code': 'ERR', 'time': timestamp, 'msg': "Failure"} # Hiding raw exception for cleaner UI
239
-
240
  with state_lock:
241
- monitoring_state[url] = result
242
 
243
  def perform_full_scan():
244
- """Iterates through all URLs and updates progress."""
245
  urls = get_all_urls()
246
-
247
  with state_lock:
248
- if scan_state['is_scanning']:
249
- return # Prevent concurrent full scans
250
- scan_state['is_scanning'] = True
251
- scan_state['total'] = len(urls)
252
- scan_state['completed'] = 0
253
-
254
  for url in urls:
255
  check_single_url(url)
256
  with state_lock:
257
  scan_state['completed'] += 1
258
-
259
  with state_lock:
260
- scan_state['is_scanning'] = False
261
- scan_state['last_scan_time'] = datetime.datetime.now().strftime("%b %d, %H:%M:%S")
262
 
263
  def background_worker():
264
- """Runs the full scan periodically."""
265
  while True:
266
  perform_full_scan()
267
  time.sleep(INTERVAL_SECONDS)
268
 
269
- # --- FLASK ROUTES (API-FIRST) ---
270
-
271
  @app.route('/')
272
- def home():
273
- """Serves the static SPA template."""
274
- return HTML_TEMPLATE
275
 
276
- @app.route('/api/status', methods=['GET'])
277
  def get_status():
278
- """Returns real-time data to the frontend."""
279
  with state_lock:
280
- state_copy = monitoring_state.copy()
281
- scan_copy = scan_state.copy()
282
-
283
- # Filter out only the DOWN urls to send back to the client
284
- down_urls = []
285
- for url, info in state_copy.items():
286
- if info['status'] != 'UP':
287
- down_urls.append({'url': url, **info})
288
-
289
- return jsonify({
290
- 'scan': scan_copy,
291
- 'down_urls': down_urls,
292
- 'total_monitored': len(state_copy)
293
- })
294
 
295
  @app.route('/api/scan', methods=['POST'])
296
- def scan_all():
297
- """Triggers a background full scan."""
298
  threading.Thread(target=perform_full_scan, daemon=True).start()
299
  return jsonify({"status": "started"})
300
 
301
  @app.route('/api/retry', methods=['POST'])
302
- def retry_url():
303
- """Retries a single URL."""
304
- data = request.json
305
- url_to_retry = data.get('url')
306
- if url_to_retry:
307
- threading.Thread(target=check_single_url, args=(url_to_retry,), daemon=True).start()
308
  return jsonify({"status": "retrying"})
309
 
310
  if __name__ == '__main__':
311
- # 1. Initialize URLs
312
- get_all_urls()
313
-
314
- # 2. Create the background thread
315
- pinger_thread = threading.Thread(target=background_worker, daemon=True)
316
- pinger_thread.start()
317
-
318
- # 3. Start the Flask server
319
- # Note: For strict production environments, replace app.run with Gunicorn/Waitress.
320
- # Because you are deploying to a specific host (like HuggingFace), keeping this as-is works perfectly.
321
- print("Server starting on port 7860...")
322
- app.run(host='0.0.0.0', port=7860)
 
3
  import requests
4
  import os
5
  import datetime
6
+ from flask import Flask, jsonify, request
7
+ from wsgiref.simple_server import make_server
8
 
9
  app = Flask(__name__)
10
 
11
  # --- CONFIGURATION ---
12
  URL_FILE = 'urls.txt'
13
  SELF_PING_URL = "https://alvin3y1-ping.hf.space/"
14
+ INTERVAL_SECONDS = 4 * 3600 # 4 hours
 
15
 
16
  # --- GLOBAL STATE ---
17
  monitoring_state = {}
 
19
  'is_scanning': False,
20
  'total': 0,
21
  'completed': 0,
22
+ 'last_scan_time': 'Never'
23
  }
24
  state_lock = threading.Lock()
25
 
26
+ # --- FRONTEND TEMPLATE ---
27
+ # Served as a static string to keep the project to one file.
28
  HTML_TEMPLATE = """
29
  <!DOCTYPE html>
30
  <html lang="en" class="dark">
31
  <head>
32
  <meta charset="UTF-8">
33
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
34
+ <title>Service Status</title>
35
  <script src="https://cdn.tailwindcss.com"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  </head>
37
+ <body class="bg-black text-gray-200 min-h-screen p-6 font-sans">
38
+ <div class="max-w-2xl mx-auto">
39
+ <div class="flex justify-between items-center mb-8 border-b border-gray-800 pb-4">
40
+ <h1 class="text-xl font-bold">System Status</h1>
41
+ <button onclick="triggerScan()" id="scan-btn" class="bg-gray-800 hover:bg-gray-700 px-3 py-1 text-sm rounded transition">Run Scan</button>
 
 
 
 
 
 
42
  </div>
43
+
44
+ <div id="progress" class="hidden mb-6">
45
+ <div class="w-full bg-gray-800 h-1 rounded-full overflow-hidden">
46
+ <div id="bar" class="bg-blue-500 h-full w-0 transition-all duration-300"></div>
 
 
 
 
47
  </div>
48
  </div>
49
 
50
+ <div id="alerts" class="space-y-3"></div>
 
51
  </div>
52
 
53
  <script>
 
 
54
  async function fetchStatus() {
55
+ const res = await fetch('/api/status');
56
+ const data = await res.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
+ // Handle Progress
59
+ const prog = document.getElementById('progress');
60
+ if (data.scan.is_scanning) {
61
+ prog.classList.remove('hidden');
62
+ const percent = (data.scan.completed / data.scan.total) * 100;
63
+ document.getElementById('bar').style.width = percent + '%';
 
 
 
 
 
 
64
  } else {
65
+ prog.classList.add('hidden');
 
 
66
  }
67
 
68
+ // Handle Down URLs
69
+ const container = document.getElementById('alerts');
70
+ container.innerHTML = data.down_urls.length ? '' : '<p class="text-green-500 text-center py-10">All Systems Operational</p>';
71
+
72
+ data.down_urls.forEach(u => {
73
+ container.innerHTML += `
74
+ <div class="bg-red-950/20 border border-red-900/50 p-4 rounded flex justify-between items-center">
75
+ <div>
76
+ <div class="text-sm font-mono text-red-200">${u.url}</div>
77
+ <div class="text-xs text-red-500 mt-1">${u.msg} (${u.code})</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  </div>
79
+ <button onclick="retry('${u.url}')" class="text-xs bg-red-900 px-2 py-1 rounded">Retry</button>
80
+ </div>`;
81
+ });
82
+ }
83
+ async function triggerScan() {
84
+ await fetch('/api/scan', {method: 'POST'});
85
+ fetchStatus();
86
+ }
87
+ async function retry(url) {
88
+ await fetch('/api/retry', {method: 'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({url})});
89
+ fetchStatus();
90
  }
 
 
91
  setInterval(fetchStatus, 2000);
 
 
92
  fetchStatus();
93
  </script>
94
  </body>
95
  </html>
96
  """
97
 
98
+ # --- LOGIC ---
99
  def get_all_urls():
100
+ urls = [SELF_PING_URL]
 
101
  if os.path.exists(URL_FILE):
102
  with open(URL_FILE, 'r') as f:
103
+ urls.extend([l.strip() for l in f if l.strip()])
104
+ return list(set(urls))
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
  def check_single_url(url):
107
+ timestamp = datetime.datetime.now().strftime("%H:%M:%S")
 
 
 
108
  try:
109
+ r = requests.get(url, timeout=10)
110
+ res = {'status': 'UP' if r.status_code < 400 else 'DOWN', 'code': r.status_code, 'time': timestamp, 'msg': 'OK' if r.status_code < 400 else 'Error'}
 
 
 
 
 
 
 
111
  except Exception as e:
112
+ res = {'status': 'DOWN', 'code': 'ERR', 'time': timestamp, 'msg': str(e)[:20]}
 
113
  with state_lock:
114
+ monitoring_state[url] = res
115
 
116
  def perform_full_scan():
 
117
  urls = get_all_urls()
 
118
  with state_lock:
119
+ scan_state.update({'is_scanning': True, 'total': len(urls), 'completed': 0})
 
 
 
 
 
120
  for url in urls:
121
  check_single_url(url)
122
  with state_lock:
123
  scan_state['completed'] += 1
 
124
  with state_lock:
125
+ scan_state.update({'is_scanning': False, 'last_scan_time': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")})
 
126
 
127
  def background_worker():
 
128
  while True:
129
  perform_full_scan()
130
  time.sleep(INTERVAL_SECONDS)
131
 
132
+ # --- ROUTES ---
 
133
  @app.route('/')
134
+ def home(): return HTML_TEMPLATE
 
 
135
 
136
+ @app.route('/api/status')
137
  def get_status():
 
138
  with state_lock:
139
+ down = [{'url': u, **s} for u, s in monitoring_state.items() if s['status'] == 'DOWN']
140
+ return jsonify({'scan': scan_state, 'down_urls': down})
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
  @app.route('/api/scan', methods=['POST'])
143
+ def scan():
 
144
  threading.Thread(target=perform_full_scan, daemon=True).start()
145
  return jsonify({"status": "started"})
146
 
147
  @app.route('/api/retry', methods=['POST'])
148
+ def retry():
149
+ threading.Thread(target=check_single_url, args=(request.json['url'],), daemon=True).start()
 
 
 
 
150
  return jsonify({"status": "retrying"})
151
 
152
  if __name__ == '__main__':
153
+ threading.Thread(target=background_worker, daemon=True).start()
154
+ print("Server starting on http://0.0.0.0:7860")
155
+ make_server('0.0.0.0', 7860, app).serve_forever()