LogicalTrue commited on
Commit
bdb76da
·
1 Parent(s): e791e9a

feat: sync active background generation status across devices and sessions via /api/user-active-job

Browse files
Files changed (3) hide show
  1. backend/server.py +44 -1
  2. frontend/app.js +56 -0
  3. frontend/viewer.html +56 -0
backend/server.py CHANGED
@@ -789,6 +789,8 @@ class F23DHTTPRequestHandler(SimpleHTTPRequestHandler):
789
  self.handle_space_status()
790
  elif self.path.startswith('/api/job-status'):
791
  self.handle_job_status()
 
 
792
  elif self.path == '/api/me':
793
  username = self.get_logged_in_user()
794
  credits = 0
@@ -1708,7 +1710,7 @@ class F23DHTTPRequestHandler(SimpleHTTPRequestHandler):
1708
  "status": row[3],
1709
  "progress": row[4],
1710
  "message": row[5],
1711
- "result": json.loads(row[6]) if row[6] else None
1712
  }
1713
 
1714
  self.send_response(200)
@@ -1720,6 +1722,47 @@ class F23DHTTPRequestHandler(SimpleHTTPRequestHandler):
1720
  print(f"[Backend Error in handle_job_status] {e}")
1721
  self.send_error_response(500, str(e))
1722
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1723
  def handle_get_gallery(self):
1724
  try:
1725
  username = self.get_logged_in_user()
 
789
  self.handle_space_status()
790
  elif self.path.startswith('/api/job-status'):
791
  self.handle_job_status()
792
+ elif self.path == '/api/user-active-job':
793
+ self.handle_user_active_job()
794
  elif self.path == '/api/me':
795
  username = self.get_logged_in_user()
796
  credits = 0
 
1710
  "status": row[3],
1711
  "progress": row[4],
1712
  "message": row[5],
1713
+ "result": json.loads(row[6]) if row[6] and (row[6].startswith('{') or row[6].startswith('[')) else row[6]
1714
  }
1715
 
1716
  self.send_response(200)
 
1722
  print(f"[Backend Error in handle_job_status] {e}")
1723
  self.send_error_response(500, str(e))
1724
 
1725
+ def handle_user_active_job(self):
1726
+ try:
1727
+ username = self.get_logged_in_user()
1728
+ if not username:
1729
+ self.send_error_response(401, "No has iniciado sesión.")
1730
+ return
1731
+
1732
+ conn = get_db_connection()
1733
+ cursor = conn.cursor()
1734
+ cursor.execute(
1735
+ "SELECT id, username, type, status, progress, message, result FROM jobs WHERE username = ? AND status IN ('pending', 'processing') ORDER BY created_at DESC LIMIT 1",
1736
+ (username,)
1737
+ )
1738
+ row = cursor.fetchone()
1739
+ conn.close()
1740
+
1741
+ if not row:
1742
+ self.send_response(200)
1743
+ self.send_header('Content-Type', 'application/json')
1744
+ self.end_headers()
1745
+ self.wfile.write(json.dumps({"has_active": False}).encode('utf-8'))
1746
+ return
1747
+
1748
+ job_data = {
1749
+ "has_active": True,
1750
+ "job_id": row[0],
1751
+ "username": row[1],
1752
+ "type": row[2],
1753
+ "status": row[3],
1754
+ "progress": row[4],
1755
+ "message": row[5],
1756
+ "result": json.loads(row[6]) if row[6] and (row[6].startswith('{') or row[6].startswith('[')) else row[6]
1757
+ }
1758
+
1759
+ self.send_response(200)
1760
+ self.send_header('Content-Type', 'application/json')
1761
+ self.end_headers()
1762
+ self.wfile.write(json.dumps(job_data).encode('utf-8'))
1763
+ except Exception as e:
1764
+ self.send_error_response(500, str(e))
1765
+
1766
  def handle_get_gallery(self):
1767
  try:
1768
  username = self.get_logged_in_user()
frontend/app.js CHANGED
@@ -244,11 +244,67 @@ async function refreshLibraryThumbnails() {
244
  const data = await response.json();
245
  cachedGalleryItems = data.items || [];
246
  renderLibraryThumbnails(cachedGalleryItems);
 
247
  } catch (e) {
248
  console.error('Error loading library thumbnails:', e);
249
  }
250
  }
251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  function renderLibraryThumbnails(items) {
253
  if (!libraryThumbnailGrid) return;
254
 
 
244
  const data = await response.json();
245
  cachedGalleryItems = data.items || [];
246
  renderLibraryThumbnails(cachedGalleryItems);
247
+ checkActiveUserJob();
248
  } catch (e) {
249
  console.error('Error loading library thumbnails:', e);
250
  }
251
  }
252
 
253
+ let isPollingActiveJobPC = false;
254
+ async function checkActiveUserJob() {
255
+ if (isPollingActiveJobPC) return;
256
+ try {
257
+ const res = await fetch(`${apiBase}/api/user-active-job`);
258
+ if (!res.ok) return;
259
+ const data = await res.json();
260
+ if (data.has_active && data.job_id) {
261
+ isPollingActiveJobPC = true;
262
+ resumeActiveJobPollingPC(data.job_id, data.type);
263
+ }
264
+ } catch (e) {
265
+ console.warn('[PC] Error checking active job:', e);
266
+ }
267
+ }
268
+
269
+ function resumeActiveJobPollingPC(jobId, jobType) {
270
+ const bgTaskBanner = document.getElementById('bgTaskBanner');
271
+ const bgTaskText = document.getElementById('bgTaskText');
272
+ const typeLabel = jobType === '2d' ? '2D' : '3D';
273
+
274
+ if (bgTaskBanner && bgTaskText) {
275
+ bgTaskText.textContent = `Generando ${typeLabel}... 0%`;
276
+ bgTaskBanner.style.display = 'flex';
277
+ }
278
+
279
+ const pollInterval = setInterval(async () => {
280
+ try {
281
+ const statusRes = await fetch(`${apiBase}/api/job-status?job_id=${jobId}`);
282
+ if (!statusRes.ok) return;
283
+ const job = await statusRes.json();
284
+
285
+ if (job.status === 'processing' || job.status === 'pending') {
286
+ const progress = job.progress || 10;
287
+ if (bgTaskBanner && bgTaskText) {
288
+ bgTaskText.textContent = `Generando ${typeLabel}... ${progress}%`;
289
+ bgTaskBanner.style.display = 'flex';
290
+ }
291
+ } else if (job.status === 'completed') {
292
+ clearInterval(pollInterval);
293
+ isPollingActiveJobPC = false;
294
+ if (bgTaskBanner) bgTaskBanner.style.display = 'none';
295
+ try { refreshGallery(); } catch (e) {}
296
+ try { fetchUserInfo(); } catch (e) {}
297
+ } else if (job.status === 'failed') {
298
+ clearInterval(pollInterval);
299
+ isPollingActiveJobPC = false;
300
+ if (bgTaskBanner) bgTaskBanner.style.display = 'none';
301
+ }
302
+ } catch (e) {
303
+ console.warn('[PC] Active job poll warning:', e);
304
+ }
305
+ }, 2500);
306
+ }
307
+
308
  function renderLibraryThumbnails(items) {
309
  if (!libraryThumbnailGrid) return;
310
 
frontend/viewer.html CHANGED
@@ -1084,6 +1084,7 @@
1084
  galleryItems = data.items || [];
1085
  renderGallery();
1086
  renderMobGalleryImageGrid();
 
1087
  } catch (err) {
1088
  console.error(err);
1089
  if (galleryGrid) {
@@ -1092,6 +1093,61 @@
1092
  }
1093
  }
1094
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1095
  function renderGallery() {
1096
  const filtered = galleryItems.filter(item => {
1097
  if (currentGalleryFilter === 'all') return true;
 
1084
  galleryItems = data.items || [];
1085
  renderGallery();
1086
  renderMobGalleryImageGrid();
1087
+ checkActiveUserJob();
1088
  } catch (err) {
1089
  console.error(err);
1090
  if (galleryGrid) {
 
1093
  }
1094
  }
1095
 
1096
+ let isPollingActiveJob = false;
1097
+ async function checkActiveUserJob() {
1098
+ if (isPollingActiveJob) return;
1099
+ try {
1100
+ const res = await fetch('/api/user-active-job');
1101
+ if (!res.ok) return;
1102
+ const data = await res.json();
1103
+ if (data.has_active && data.job_id) {
1104
+ isPollingActiveJob = true;
1105
+ resumeActiveJobPolling(data.job_id, data.type);
1106
+ }
1107
+ } catch (e) {
1108
+ console.warn('[Viewer] Error checking active job:', e);
1109
+ }
1110
+ }
1111
+
1112
+ function resumeActiveJobPolling(jobId, jobType) {
1113
+ const mobBgTaskBanner = document.getElementById('mobBgTaskBanner');
1114
+ const mobBgTaskText = document.getElementById('mobBgTaskText');
1115
+ const typeLabel = jobType === '2d' ? '2D' : '3D';
1116
+
1117
+ if (mobBgTaskBanner && mobBgTaskText) {
1118
+ mobBgTaskText.textContent = `Generando ${typeLabel}... 0%`;
1119
+ mobBgTaskBanner.style.display = 'flex';
1120
+ }
1121
+
1122
+ const pollInterval = setInterval(async () => {
1123
+ try {
1124
+ const statusRes = await fetch(`/api/job-status?job_id=${jobId}`);
1125
+ if (!statusRes.ok) return;
1126
+ const job = await statusRes.json();
1127
+
1128
+ if (job.status === 'processing' || job.status === 'pending') {
1129
+ const progress = job.progress || 10;
1130
+ if (mobBgTaskBanner && mobBgTaskText) {
1131
+ mobBgTaskText.textContent = `Generando ${typeLabel}... ${progress}%`;
1132
+ mobBgTaskBanner.style.display = 'flex';
1133
+ }
1134
+ } else if (job.status === 'completed') {
1135
+ clearInterval(pollInterval);
1136
+ isPollingActiveJob = false;
1137
+ if (mobBgTaskBanner) mobBgTaskBanner.style.display = 'none';
1138
+ try { fetchGallery(); } catch (e) {}
1139
+ try { fetchUserInfo(); } catch (e) {}
1140
+ } else if (job.status === 'failed') {
1141
+ clearInterval(pollInterval);
1142
+ isPollingActiveJob = false;
1143
+ if (mobBgTaskBanner) mobBgTaskBanner.style.display = 'none';
1144
+ }
1145
+ } catch (e) {
1146
+ console.warn('[Viewer] Active job poll warning:', e);
1147
+ }
1148
+ }, 2500);
1149
+ }
1150
+
1151
  function renderGallery() {
1152
  const filtered = galleryItems.filter(item => {
1153
  if (currentGalleryFilter === 'all') return true;