Spaces:
Running
Running
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- backend/server.py +44 -1
- frontend/app.js +56 -0
- 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
|
| 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;
|