abdulsalam2121 commited on
Commit
2cd7bd9
·
1 Parent(s): 38a89cd

Enhance error handling and logging during authentication and navigation processes

Browse files
app/auth_handler.py CHANGED
@@ -162,6 +162,25 @@ class AuthHandler:
162
  except Exception:
163
  return False
164
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  def login(self) -> bool:
166
  page = self.page
167
  logger.info("Attempting login")
@@ -236,6 +255,11 @@ class AuthHandler:
236
 
237
  deadline = time.time() + 20
238
  while time.time() < deadline:
 
 
 
 
 
239
  self.handle_age_gate()
240
  if self.is_logged_in():
241
  logger.info("Login successful")
 
162
  except Exception:
163
  return False
164
 
165
+ def _login_failed_visible(self) -> bool:
166
+ page = self.page
167
+ try:
168
+ body_text = page.locator("body").inner_text(timeout=1500).lower()
169
+ except Exception:
170
+ try:
171
+ body_text = page.content().lower()
172
+ except Exception:
173
+ return False
174
+
175
+ failure_markers = [
176
+ "login failed",
177
+ "unable to authenticate",
178
+ "please try your login again",
179
+ "invalid username",
180
+ "incorrect username or password",
181
+ ]
182
+ return any(marker in body_text for marker in failure_markers)
183
+
184
  def login(self) -> bool:
185
  page = self.page
186
  logger.info("Attempting login")
 
255
 
256
  deadline = time.time() + 20
257
  while time.time() < deadline:
258
+ if self._login_failed_visible():
259
+ logger.error("Login failed page detected")
260
+ self.session.screenshot("login_failed")
261
+ return False
262
+
263
  self.handle_age_gate()
264
  if self.is_logged_in():
265
  logger.info("Login successful")
app/bot.py CHANGED
@@ -88,12 +88,14 @@ class BotRunner:
88
  studio_url = navigator._ensure_price_sort(studio_url)
89
  self._log(f"Direct studio URL detected, navigating directly: {studio_url}")
90
  if not navigator.navigate_to_studio_url(studio_url):
91
- raise RuntimeError("Could not open studio page")
 
92
  else:
93
  self._log(f"Studio name detected, searching directory: {studio_input}")
94
  studio_url = navigator.find_studio_by_name(studio_input)
95
  if not studio_url:
96
- raise RuntimeError(f"Could not find studio: {studio_input}")
 
97
 
98
  self._log("Phase 3 ▶ Listing scan")
99
  listing = ListingScraper(
 
88
  studio_url = navigator._ensure_price_sort(studio_url)
89
  self._log(f"Direct studio URL detected, navigating directly: {studio_url}")
90
  if not navigator.navigate_to_studio_url(studio_url):
91
+ self._log("Could not open studio page")
92
+ return
93
  else:
94
  self._log(f"Studio name detected, searching directory: {studio_input}")
95
  studio_url = navigator.find_studio_by_name(studio_input)
96
  if not studio_url:
97
+ self._log(f"Could not find studio: {studio_input}")
98
+ return
99
 
100
  self._log("Phase 3 ▶ Listing scan")
101
  listing = ListingScraper(
app/browser_session.py CHANGED
@@ -1,4 +1,5 @@
1
  import logging
 
2
  from pathlib import Path
3
  from playwright.sync_api import sync_playwright, Browser, BrowserContext, Page
4
  from config import HOME_URL
@@ -22,6 +23,17 @@ class BrowserSession:
22
  self._context: BrowserContext = None
23
  self.page: Page = None
24
 
 
 
 
 
 
 
 
 
 
 
 
25
  def start(self):
26
  self.state_path.parent.mkdir(exist_ok=True)
27
  self.logs_dir.mkdir(exist_ok=True, parents=True)
@@ -75,60 +87,55 @@ class BrowserSession:
75
  logger.info("Browser session closed")
76
 
77
  def goto(self, url: str, wait_until: str = "domcontentloaded", timeout_override: int = None) -> bool:
78
- try:
79
- # Use override timeout or default
80
- nav_timeout = timeout_override if timeout_override else self.timeout
81
-
82
- response = self.page.goto(url, wait_until=wait_until, timeout=nav_timeout)
83
- # If we have a response object, check HTTP status
84
- if response is not None:
85
- status = response.status
86
- if status >= 400:
87
- logger.error(f"Navigation returned HTTP {status} for {url}")
88
- # Save context for debugging
89
- try:
90
- self.screenshot(f"http_{status}")
91
- html = self.page.content()
92
- Path("logs").mkdir(exist_ok=True)
93
- Path(f"logs/http_{status}.html").write_text(html, encoding="utf-8")
94
- except Exception:
95
- pass
96
- return False
97
- try:
98
- # Update the latest screenshot after successful navigation for debugging/UI
99
- self.screenshot("latest")
100
- except Exception:
101
- pass
102
- return True
103
- except Exception as e:
104
- logger.error(f"Navigation failed [{url}]: {e}")
105
  try:
106
- self.screenshot("navigation_error")
107
- except Exception:
108
- pass
109
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  def cleanup_page(self):
112
  """
113
- Cleanup page resources to prevent memory accumulation during long pagination runs.
114
- Clears local storage, session storage, and forces garbage collection.
 
 
 
115
  """
116
  try:
117
  if self.page:
118
- # Clear storages and cleanup DOM
119
- self.page.evaluate("""
120
- () => {
121
- localStorage.clear();
122
- sessionStorage.clear();
123
- // Purge cache
124
- if (window.caches) {
125
- caches.keys().then(names => {
126
- names.forEach(name => caches.delete(name));
127
- });
128
- }
129
- }
130
- """)
131
- logger.debug("Cleared page storage and cache")
132
  except Exception as e:
133
  logger.debug(f"Page cleanup warning (non-critical): {e}")
134
 
 
1
  import logging
2
+ import time
3
  from pathlib import Path
4
  from playwright.sync_api import sync_playwright, Browser, BrowserContext, Page
5
  from config import HOME_URL
 
23
  self._context: BrowserContext = None
24
  self.page: Page = None
25
 
26
+ def _ensure_page(self) -> Page:
27
+ try:
28
+ if self.page is None or self.page.is_closed():
29
+ self.page = self._context.new_page()
30
+ except Exception:
31
+ try:
32
+ self.page = self._context.new_page()
33
+ except Exception:
34
+ return None
35
+ return self.page
36
+
37
  def start(self):
38
  self.state_path.parent.mkdir(exist_ok=True)
39
  self.logs_dir.mkdir(exist_ok=True, parents=True)
 
87
  logger.info("Browser session closed")
88
 
89
  def goto(self, url: str, wait_until: str = "domcontentloaded", timeout_override: int = None) -> bool:
90
+ nav_timeout = timeout_override if timeout_override else self.timeout
91
+
92
+ for attempt in range(1, 3):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  try:
94
+ page = self._ensure_page()
95
+ if page is None:
96
+ raise RuntimeError("Browser page is not available")
97
+
98
+ response = page.goto(url, wait_until=wait_until, timeout=nav_timeout)
99
+ if response is not None:
100
+ status = response.status
101
+ if status >= 400:
102
+ logger.error(f"Navigation returned HTTP {status} for {url}")
103
+ try:
104
+ self.screenshot(f"http_{status}")
105
+ html = page.content()
106
+ Path("logs").mkdir(exist_ok=True)
107
+ Path(f"logs/http_{status}.html").write_text(html, encoding="utf-8")
108
+ except Exception:
109
+ pass
110
+ return False
111
+
112
+ try:
113
+ self.screenshot("latest")
114
+ except Exception:
115
+ pass
116
+ return True
117
+ except Exception as e:
118
+ logger.error(f"Navigation failed [{url}] attempt {attempt}/2: {e}")
119
+ try:
120
+ self.screenshot("navigation_error")
121
+ except Exception:
122
+ pass
123
+ if attempt < 2:
124
+ time.sleep(1)
125
+ continue
126
+ return False
127
 
128
  def cleanup_page(self):
129
  """
130
+ Lightweight cleanup hook for long runs.
131
+
132
+ The previous implementation cleared browser storage on every page, which
133
+ can invalidate authenticated site state. Keep this method non-destructive
134
+ so it cannot disrupt the session mid-run.
135
  """
136
  try:
137
  if self.page:
138
+ logger.debug("Page cleanup skipped to preserve authenticated state")
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  except Exception as e:
140
  logger.debug(f"Page cleanup warning (non-critical): {e}")
141
 
app/listing_scraper.py CHANGED
@@ -1,6 +1,5 @@
1
  import logging
2
  import re
3
- import threading
4
  import time
5
  from typing import Callable, Any, Dict, List, Optional
6
  from urllib.parse import urljoin
@@ -79,84 +78,48 @@ class ListingScraper:
79
  logger.debug("Extracting products from div.row structure")
80
 
81
  try:
82
- # Timeout extraction with a thread-based approach
83
- extraction_timeout = self.page_timeout - 5 # Leave 5s buffer
84
- result_container = {"products": [], "error": None, "done": False}
85
-
86
- def extract_products():
87
  try:
88
- rows = page.locator("div.row").all()
89
- logger.debug(f"Found {len(rows)} potential product rows")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
- for idx, row in enumerate(rows):
92
- try:
93
- # Extract title from div.caption h4 a
94
- try:
95
- title_elem = row.locator("div.caption h4 a").first
96
- title = (title_elem.inner_text(timeout=500) or "").strip()
97
- link = title_elem.get_attribute("href") or ""
98
- except Exception:
99
- logger.debug(f"Row {idx}: Could not extract title")
100
- continue
101
-
102
- if not title or not link:
103
- logger.debug(f"Row {idx}: Missing title or link")
104
- continue
105
-
106
- # Extract price from div.price strong
107
- price = None
108
- try:
109
- price_elem = row.locator("div.price strong").first
110
- price_text = (price_elem.inner_text(timeout=500) or "").strip()
111
- price = _parse_price(price_text)
112
- logger.debug(f"Row {idx}: Extracted price text: {price_text} -> ${price}")
113
- except Exception as e:
114
- logger.debug(f"Row {idx}: Could not extract price: {e}")
115
-
116
- # Skip if price is unknown
117
- if price is None:
118
- self.skipped_unknown_price += 1
119
- logger.debug(f"Row {idx}: Skipping '{title}' - no price found")
120
- continue
121
-
122
- # Filter by minimum price - ONLY collect items >= min_price
123
- if price < self.min_price:
124
- self.skipped_below_threshold += 1
125
- logger.debug(f"Row {idx}: Skip '{title}' (${price:.2f} < ${self.min_price:.2f})")
126
- continue
127
-
128
- # This product qualifies
129
- logger.debug(f"Row {idx}: ✓ QUALIFY '{title}' @ ${price:.2f}")
130
- result_container["products"].append({
131
- "url": link,
132
- "title": title,
133
- "price": price
134
- })
135
-
136
- except Exception as e:
137
- logger.debug(f"Row {idx}: Error processing row: {e}")
138
- continue
139
-
140
  except Exception as e:
141
- result_container["error"] = str(e)
142
- finally:
143
- result_container["done"] = True
144
-
145
- # Run extraction in thread with timeout
146
- thread = threading.Thread(target=extract_products, daemon=True)
147
- thread.start()
148
- thread.join(timeout=extraction_timeout)
149
-
150
- if result_container["done"]:
151
- if result_container["error"]:
152
- logger.error(f"Product extraction error: {result_container['error']}")
153
- products = result_container["products"]
154
- else:
155
- logger.warning(f"Product extraction timed out after {extraction_timeout}s")
156
- try:
157
- self.session.screenshot("extraction_timeout")
158
- except Exception:
159
- pass
160
 
161
  logger.info(f"Extracted {len(products)} qualifying product(s) from page")
162
  return products
@@ -292,27 +255,11 @@ class ListingScraper:
292
  return next_url
293
 
294
  def _resolve_next_page_url_with_timeout(self, current_url: str, timeout_ms: int = 5000) -> Optional[str]:
295
- """Resolve next page URL with timeout protection to prevent hangs."""
296
- result = {"url": None, "done": False, "error": None}
297
-
298
- def resolve_thread():
299
- try:
300
- result["url"] = self._resolve_next_page_url(current_url)
301
- except Exception as e:
302
- logger.warning(f"Error resolving next page URL: {e}")
303
- result["error"] = str(e)
304
- finally:
305
- result["done"] = True
306
-
307
- thread = threading.Thread(target=resolve_thread, daemon=True)
308
- thread.daemon = True
309
- thread.start()
310
- thread.join(timeout=timeout_ms / 1000.0) # Convert to seconds
311
-
312
- if result["done"]:
313
- return result["url"]
314
- else:
315
- logger.warning(f"Next page URL resolution timed out after {timeout_ms}ms - stopping pagination")
316
  return None
317
 
318
  def _load_page_with_retry(self, url: str, page_num: int, max_retries: int = 3) -> bool:
@@ -325,38 +272,16 @@ class ListingScraper:
325
  for attempt in range(1, max_retries + 1):
326
  try:
327
  logger.info(f"Page load attempt {attempt}/{max_retries} for page {page_num}: {url}")
328
-
329
- # Load page with timeout thread
330
- load_result = {"success": False, "done": False}
331
-
332
- def load_thread():
333
- try:
334
- load_result["success"] = self.session.goto(url)
335
- except Exception as e:
336
- logger.warning(f"Navigation exception: {e}")
337
- finally:
338
- load_result["done"] = True
339
-
340
- thread = threading.Thread(target=load_thread, daemon=True)
341
- thread.start()
342
- thread.join(timeout=self.page_timeout)
343
-
344
- if load_result["done"] and load_result["success"]:
345
  logger.info(f"✓ Successfully loaded page {page_num} on attempt {attempt}")
346
- # Aggressive cleanup after successful navigation
347
  try:
348
  self.session.cleanup_page()
349
- # Force garbage collection
350
  import gc
351
  gc.collect()
352
  except Exception as e:
353
  logger.debug(f"Cleanup warning (non-critical): {e}")
354
  return True
355
- else:
356
- if not load_result["done"]:
357
- logger.warning(f"✗ Page load timed out after {self.page_timeout}s for page {page_num}, attempt {attempt}")
358
- else:
359
- logger.warning(f"✗ Page load failed for page {page_num}, attempt {attempt}")
360
  except Exception as e:
361
  logger.warning(f"✗ Exception during page load for page {page_num}, attempt {attempt}: {e}")
362
 
 
1
  import logging
2
  import re
 
3
  import time
4
  from typing import Callable, Any, Dict, List, Optional
5
  from urllib.parse import urljoin
 
78
  logger.debug("Extracting products from div.row structure")
79
 
80
  try:
81
+ rows = page.locator("div.row").all()
82
+ logger.debug(f"Found {len(rows)} potential product rows")
83
+
84
+ for idx, row in enumerate(rows):
 
85
  try:
86
+ try:
87
+ title_elem = row.locator("div.caption h4 a").first
88
+ title = (title_elem.inner_text(timeout=500) or "").strip()
89
+ link = title_elem.get_attribute("href") or ""
90
+ except Exception:
91
+ logger.debug(f"Row {idx}: Could not extract title")
92
+ continue
93
+
94
+ if not title or not link:
95
+ logger.debug(f"Row {idx}: Missing title or link")
96
+ continue
97
+
98
+ price = None
99
+ try:
100
+ price_elem = row.locator("div.price strong").first
101
+ price_text = (price_elem.inner_text(timeout=500) or "").strip()
102
+ price = _parse_price(price_text)
103
+ logger.debug(f"Row {idx}: Extracted price text: {price_text} -> ${price}")
104
+ except Exception as e:
105
+ logger.debug(f"Row {idx}: Could not extract price: {e}")
106
+
107
+ if price is None:
108
+ self.skipped_unknown_price += 1
109
+ logger.debug(f"Row {idx}: Skipping '{title}' - no price found")
110
+ continue
111
+
112
+ if price < self.min_price:
113
+ self.skipped_below_threshold += 1
114
+ logger.debug(f"Row {idx}: Skip '{title}' (${price:.2f} < ${self.min_price:.2f})")
115
+ continue
116
+
117
+ logger.debug(f"Row {idx}: ✓ QUALIFY '{title}' @ ${price:.2f}")
118
+ products.append({"url": link, "title": title, "price": price})
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  except Exception as e:
121
+ logger.debug(f"Row {idx}: Error processing row: {e}")
122
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  logger.info(f"Extracted {len(products)} qualifying product(s) from page")
125
  return products
 
255
  return next_url
256
 
257
  def _resolve_next_page_url_with_timeout(self, current_url: str, timeout_ms: int = 5000) -> Optional[str]:
258
+ """Resolve next page URL without crossing thread boundaries."""
259
+ try:
260
+ return self._resolve_next_page_url(current_url)
261
+ except Exception as e:
262
+ logger.warning(f"Error resolving next page URL: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  return None
264
 
265
  def _load_page_with_retry(self, url: str, page_num: int, max_retries: int = 3) -> bool:
 
272
  for attempt in range(1, max_retries + 1):
273
  try:
274
  logger.info(f"Page load attempt {attempt}/{max_retries} for page {page_num}: {url}")
275
+ if self.session.goto(url):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  logger.info(f"✓ Successfully loaded page {page_num} on attempt {attempt}")
 
277
  try:
278
  self.session.cleanup_page()
 
279
  import gc
280
  gc.collect()
281
  except Exception as e:
282
  logger.debug(f"Cleanup warning (non-critical): {e}")
283
  return True
284
+ logger.warning(f"✗ Page load failed for page {page_num}, attempt {attempt}")
 
 
 
 
285
  except Exception as e:
286
  logger.warning(f"✗ Exception during page load for page {page_num}, attempt {attempt}: {e}")
287
 
app/product_scraper.py CHANGED
@@ -1,7 +1,6 @@
1
  import logging
2
  import re
3
  import time
4
- import threading
5
  from datetime import datetime
6
  from typing import Any, Dict, Optional
7
  from urllib.parse import urljoin
@@ -25,85 +24,36 @@ class ProductScraper:
25
  return self.session.page
26
 
27
  def _meta_value_safe(self, label: str, timeout: int = 8) -> Optional[str]:
28
- """Extract metadata value with timeout protection."""
29
- result = {"value": None, "done": False}
30
-
31
- def extract():
32
- try:
33
- result["value"] = self._meta_value(label)
34
- except Exception as e:
35
- logger.debug(f"Meta value extraction error for '{label}': {e}")
36
- finally:
37
- result["done"] = True
38
-
39
- thread = threading.Thread(target=extract, daemon=True)
40
- thread.start()
41
- thread.join(timeout=timeout)
42
-
43
- if not result["done"]:
44
- logger.warning(f"Meta value extraction timed out for '{label}'")
45
  return None
46
- return result["value"]
47
 
48
  def _get_title_safe(self, timeout: int = 8) -> str:
49
- """Extract title with timeout protection."""
50
- result = {"value": "", "done": False}
51
-
52
- def extract():
53
- try:
54
- result["value"] = self._get_title()
55
- except Exception as e:
56
- logger.debug(f"Title extraction error: {e}")
57
- finally:
58
- result["done"] = True
59
-
60
- thread = threading.Thread(target=extract, daemon=True)
61
- thread.start()
62
- thread.join(timeout=timeout)
63
-
64
- if not result["done"]:
65
- logger.warning("Title extraction timed out")
66
- return result["value"]
67
 
68
  def _get_price_safe(self, timeout: int = 8) -> str:
69
- """Extract price with timeout protection."""
70
- result = {"value": "", "done": False}
71
-
72
- def extract():
73
- try:
74
- result["value"] = self._get_price()
75
- except Exception as e:
76
- logger.debug(f"Price extraction error: {e}")
77
- finally:
78
- result["done"] = True
79
-
80
- thread = threading.Thread(target=extract, daemon=True)
81
- thread.start()
82
- thread.join(timeout=timeout)
83
-
84
- if not result["done"]:
85
- logger.warning("Price extraction timed out")
86
- return result["value"]
87
 
88
  def _get_category_safe(self, timeout: int = 8) -> str:
89
- """Extract category with timeout protection."""
90
- result = {"value": "", "done": False}
91
-
92
- def extract():
93
- try:
94
- result["value"] = self._get_category()
95
- except Exception as e:
96
- logger.debug(f"Category extraction error: {e}")
97
- finally:
98
- result["done"] = True
99
-
100
- thread = threading.Thread(target=extract, daemon=True)
101
- thread.start()
102
- thread.join(timeout=timeout)
103
-
104
- if not result["done"]:
105
- logger.warning("Category extraction timed out")
106
- return result["value"]
107
 
108
  # ------------------------------------------------------------------
109
  # Metadata extraction
@@ -237,22 +187,8 @@ class ProductScraper:
237
  return result
238
 
239
  logger.debug(f"Scraping (attempt {attempt}): {url}")
240
-
241
- # Navigate with timeout protection
242
- nav_success = False
243
- nav_result = {"success": False, "done": False}
244
-
245
- def navigate():
246
- try:
247
- nav_result["success"] = self.session.goto(url)
248
- finally:
249
- nav_result["done"] = True
250
-
251
- thread = threading.Thread(target=navigate, daemon=True)
252
- thread.start()
253
- thread.join(timeout=self.product_timeout)
254
-
255
- if not nav_result["done"] or not nav_result["success"]:
256
  raise RuntimeError("Navigation failed or timed out")
257
 
258
  time.sleep(0.4)
 
1
  import logging
2
  import re
3
  import time
 
4
  from datetime import datetime
5
  from typing import Any, Dict, Optional
6
  from urllib.parse import urljoin
 
24
  return self.session.page
25
 
26
  def _meta_value_safe(self, label: str, timeout: int = 8) -> Optional[str]:
27
+ """Extract metadata value with direct Playwright calls."""
28
+ try:
29
+ return self._meta_value(label)
30
+ except Exception as e:
31
+ logger.debug(f"Meta value extraction error for '{label}': {e}")
 
 
 
 
 
 
 
 
 
 
 
 
32
  return None
 
33
 
34
  def _get_title_safe(self, timeout: int = 8) -> str:
35
+ """Extract title with direct Playwright calls."""
36
+ try:
37
+ return self._get_title()
38
+ except Exception as e:
39
+ logger.debug(f"Title extraction error: {e}")
40
+ return ""
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  def _get_price_safe(self, timeout: int = 8) -> str:
43
+ """Extract price with direct Playwright calls."""
44
+ try:
45
+ return self._get_price()
46
+ except Exception as e:
47
+ logger.debug(f"Price extraction error: {e}")
48
+ return ""
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
  def _get_category_safe(self, timeout: int = 8) -> str:
51
+ """Extract category with direct Playwright calls."""
52
+ try:
53
+ return self._get_category()
54
+ except Exception as e:
55
+ logger.debug(f"Category extraction error: {e}")
56
+ return ""
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  # ------------------------------------------------------------------
59
  # Metadata extraction
 
187
  return result
188
 
189
  logger.debug(f"Scraping (attempt {attempt}): {url}")
190
+
191
+ if not self.session.goto(url):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  raise RuntimeError("Navigation failed or timed out")
193
 
194
  time.sleep(0.4)
app/services/run_bot.py CHANGED
@@ -196,7 +196,9 @@ class AutomationController:
196
  self._set_state(progress=10, current_state="Logging in")
197
  auth = AuthHandler(session, payload["username"], payload["password"])
198
  if not auth.ensure_authenticated():
199
- raise RuntimeError("Login failed")
 
 
200
 
201
  if self._stop_event.is_set():
202
  self._set_state(current_state="Stopped by user")
@@ -214,7 +216,9 @@ class AutomationController:
214
  if not session.goto(studio_url):
215
  logger.warning("Direct navigation failed, falling back to navigator")
216
  if not navigator.navigate_to_studio_url(studio_url):
217
- raise RuntimeError("Could not open studio page")
 
 
218
  else:
219
  time.sleep(1)
220
  logger.info("Direct studio page loaded")
@@ -227,7 +231,9 @@ class AutomationController:
227
  if not session.goto(studio_url):
228
  logger.warning("Direct navigation failed, falling back to navigator")
229
  if not navigator.navigate_to_studio_url(studio_url):
230
- raise RuntimeError("Could not open studio page")
 
 
231
  else:
232
  time.sleep(1)
233
  logger.info("Direct studio page loaded")
@@ -235,7 +241,9 @@ class AutomationController:
235
  # Treat input as a studio name and search the directory
236
  studio_url = navigator.find_studio_by_name(studio_input)
237
  if not studio_url:
238
- raise RuntimeError(f"Could not find studio: {studio_input}")
 
 
239
 
240
  if self._stop_event.is_set():
241
  self._set_state(current_state="Stopped by user")
 
196
  self._set_state(progress=10, current_state="Logging in")
197
  auth = AuthHandler(session, payload["username"], payload["password"])
198
  if not auth.ensure_authenticated():
199
+ self._set_state(last_error="Login failed", current_state="Login failed")
200
+ self.append_log("Login failed")
201
+ return
202
 
203
  if self._stop_event.is_set():
204
  self._set_state(current_state="Stopped by user")
 
216
  if not session.goto(studio_url):
217
  logger.warning("Direct navigation failed, falling back to navigator")
218
  if not navigator.navigate_to_studio_url(studio_url):
219
+ self._set_state(last_error="Could not open studio page", current_state="Studio navigation failed")
220
+ self.append_log("Could not open studio page")
221
+ return
222
  else:
223
  time.sleep(1)
224
  logger.info("Direct studio page loaded")
 
231
  if not session.goto(studio_url):
232
  logger.warning("Direct navigation failed, falling back to navigator")
233
  if not navigator.navigate_to_studio_url(studio_url):
234
+ self._set_state(last_error="Could not open studio page", current_state="Studio navigation failed")
235
+ self.append_log("Could not open studio page")
236
+ return
237
  else:
238
  time.sleep(1)
239
  logger.info("Direct studio page loaded")
 
241
  # Treat input as a studio name and search the directory
242
  studio_url = navigator.find_studio_by_name(studio_input)
243
  if not studio_url:
244
+ self._set_state(last_error=f"Could not find studio: {studio_input}", current_state="Studio not found")
245
+ self.append_log(f"Could not find studio: {studio_input}")
246
+ return
247
 
248
  if self._stop_event.is_set():
249
  self._set_state(current_state="Stopped by user")