
    HE^jM              
       2   d dl Z d dlZd dlZd dlZd dlZd dlZd dlZd dlZd dl	Z	d dl
mZmZ d dlmZ d dlmZ d dlmZ d dlmZ d dlmZ d dlmZ d d	lmZmZmZ d d
lmZ d dlm Z m!Z! d dl"m#Z# d dl$m%Z%m&Z&m'Z'm(Z( d dl)m*Z*m+Z+m,Z, da- ej\                         Z/ ej\                         Z0da1de2fdZ3de2fdZ4de5ddfdZ6de7de7de8de8ddf
dZ9de7de7de8de8ddf
dZ:de5ddfdZ; G d dejx                  ejz                  j|                        Z?d Z@d"d ZAd"d!ZBy)#    N)DictAny)settings)_log)get_db_connection)run_migrations)get_all_active_products)process_scanned_products)enrich_specsclean_ghost_listingsverify_and_enrich)backfill_psref_specs)send_startup_alertsend_once_complete_alert)LaptopTrackerHandler)send_telegram_notificationsend_telegram_batchsend_telegram_startup_alert!send_telegram_once_complete_alert)notify_new_listingnotify_back_in_stocknotify_batchFreturnc                     t         j                  d      } | sdddS 	 t        d       t               }	 |j	                         }|j                  d       |j                         d   dk(  }|j                          t               \  }}|dd	dt         j                          S t        |      }|dd
dt         j                          S t        |||      \  }}t        |||d       t               }	 |j	                         }|j                  d       |j                         d   }|j                  d       |j                         d   }	|j                          dt        |      ||	dt         j                          S # |j                          w xY w# |j                          w xY w# t        $ r:}
t        d|
        dt        |
      dcY d}
~
t         j                          S d}
~
ww xY w# t         j                          w xY w)u  Run a manual scrape process under scraper lock. Returns status dictionary.

    is_first_run is derived from the current row count (same rule
    run_scraper_loop uses at startup), not hardcoded — a manually-triggered
    scrape against an already-seeded DB must still queue and dispatch
    new/restock alerts like any other cycle, and only suppresses them on a
    genuinely empty database.
    Fblocking3A scrape or database update is already in progress.successerrorz [API] Manual scrape triggered...SELECT COUNT(*) FROM productsr   Nz,Failed to retrieve products from Lenovo API.z5Compare API unavailable. Cycle aborted (fail-closed).is_first_runpartial_scan.SELECT COUNT(*) FROM products WHERE active = 1.SELECT COUNT(*) FROM products WHERE active = 0T)r   scanned_countactive_countremoved_countz[API] Manual scrape failed: )scraper_lockacquirer   r   cursorexecutefetchonecloser	   releaser   r
   "_dispatch_notifications_backgroundlen	Exceptionstr)acquiredconnr,   r#   productsr$   new_listingsback_in_stockr(   r)   es              /home/container/backend/main.pytrigger_manual_scraper<   (   s    ##U#3H +`aa*/0 "	[[]FNN:;!??,Q/14LJJL!8!:,$/]^: 	7 %X.$/fg2 	1 '?xVbq}&~#m*<V[\ !"	[[]FNNKL!??,Q/LNNKL"OO-a0MJJL  ](*	
 	C JJL* JJL  3+A3/0 3q622	3 	sk   F* 7F  ,#F* $F* 
)F* 4AF F*  FF* F''F* *	G-3G(G-G0 (G--G0 0Hc                  l   t         j                  d      } | sdddS 	 t        d       t               }dt	        |      |dt         j                          S # t        $ r:}t        d|        dt        |      dcY d	}~t         j                          S d	}~ww xY w# t         j                          w xY w)
zIRun a manual ghost cleanup under scraper lock. Returns status dictionary.Fr   r   r   z [API] Ghost cleanup triggered...T)r   cleaned_countcleaned_codesz[API] Ghost cleanup failed: N)r*   r+   r   r   r2   r0   r3   r4   )r5   r?   r:   s      r;   trigger_ghost_cleanupr@   `   s    ##U#3H +`aa/0,. /*
 		  3+A3/0 3q622	3 	s/   #A 	B B;B<B BB B3signumc                     da t        d|  d       t        r4t        j                  t        j
                  d      j                          yy)z,Handle SIGTERM/SIGINT for graceful shutdown.TzReceived shutdown signal u   . Shutting down gracefully…targetdaemonN)_shutdown_requestedr   httpd	threadingThreadshutdownstart)rA   frames     r;   _handle_shutdownrM   s   sB     $VH,I	JKt<BBD     r8   r9   r#   silentc                    |s|ry|rQt        |      t        j                  k\  rt        |d       n(|D ]#  }t	        |d       t        j                  d       % | rQt        |       t        j                  k\  rt        | d       n(| D ]#  }t	        |d       t        j                  d       % | r-t        |       dk\  rt        | d       n| D ]  }t        |        |r.t        |      dk\  rt        |d       y|D ]  }t        |        yy)zIDispatches Telegram notifications and Web Push notifications for updates.Nrestock   added   new_listingr9   )
r2   r   NOTIFICATION_BATCH_THRESHOLDr   r   timesleepr   r   r   )r8   r9   r#   rO   ps        r;   dispatch_all_notificationsrZ   {   s    v }!F!FFy9"*1i8

1 #
 | E EEg6!*1g6

1 "
 |!}5!"1% " }"8"$Q' #	 rN   c                 `    t        j                  t        | |||fd      j                          y)uQ  Runs dispatch_all_notifications() (Telegram + Web Push) off the scrape
    loop's critical path. Both do blocking network calls per recipient —
    Telegram even sleeps 1s between individual sends below
    NOTIFICATION_BATCH_THRESHOLD — so inline they can add real seconds to
    every cycle. Discord doesn't need this: dispatch_discord_alerts/_edit
    already hand off via asyncio.run_coroutine_threadsafe onto the bot's own
    event loop thread, non-blocking already.

    Unlike the PSREF backfill below, this is never guarded/skipped even if a
    previous cycle's dispatch is still running — a specific restock/price-drop
    notification is a one-time event tied to this cycle's diff; skipping it
    isn't "retry later," it's silently dropping a user-facing alert. See
    DECISION_LOG.md's 2026-07-18 entries for the reasoning.
    TrD   argsrE   N)rH   rI   rZ   rK   )r8   r9   r#   rO   s       r;   r1   r1      s-    " )M<@ egrN   cyclec                      t         j                  d      st        d  d       y fd}t        j                  |d      j                          y)	uJ  Runs the PSREF backfill off the scrape loop's critical path — a slow or
    fully-down PSREF must never delay real Lenovo polling/notifications (see
    DECISION_LOG.md's 2026-07-18 entries). Skips spawning a new run if a
    previous one is still in flight — only possible if an unusually large
    batch of new machine types took longer than one POLL_INTERVAL to process.
    Safe to skip: unlike a notification, an MT left uncached this cycle is
    picked up automatically by the next successful run's own
    "not yet in psref_specs" query, so nothing is permanently lost.
    Fr   [Cycle zG] PSREF backfill still running from a previous cycle, skipping trigger.Nc            	          	 t               } | d   s| d   rt        d d| d    d| d    d       t        j                          y # t        j                          w xY w)Nenrichedfailedr`   z] PSREF backfill: z enriched, z failed.)r   r   psref_backfill_lockr0   )resultr^   s    r;   _runz,_run_psref_backfill_background.<locals>._run   sl    	*)+Fj!VH%5eW$6j)*+fX6F5GxQ
  '')'')s   /A ATrC   )rd   r+   r   rH   rI   rK   )r^   rf   s   ` r;   _run_psref_backfill_backgroundrg      sK     &&&6wugdef	* D.446rN   c                       e Zd ZdZy)ThreadingHTTPServerTN)__name__
__module____qualname__daemon_threads rN   r;   ri   ri      s    NrN   ri   c                    t               }	 |j                         }|j                  d       |j                         d   }|j	                          |dk(  }d}t        d       |rt        d       n1t        d| d       | j                  st        |       t        |       | j                  spddl	m
}m}  |       rat        d       t        d	      D ]=  } |       }	|	r|	j                         rt        d
        n"t        j                  d       ? t        d       t         s|dz  }	 t"        5  t%               \  }
}|
Rt'        |
      }
|
t        d| d       nTt)        |
|xs | j                  |      \  }}t+        |||xs | j                  | j                         t-        |       t               }	 |j                         }|j                  d       |j                         d   }|j                  d       |j                         d   }|j	                          t        d| d| d| dt/        |
              |rot        dt/        |
       d       | j                  s(t        t/        |
             t        t/        |
             d}n t        d| dt0        j2                   d       ddd       t        t0        j2                        D ]  }t         r nt        j                  d       ! t         st        d       y# |j	                          w xY w# |j	                          w xY w# 1 sw Y   xY w# t4        $ r}t        d| d|        Y d}~d}~ww xY w)z9Scraper daemon loop that runs inside a background thread.r!   r   u'   Initializing background scraper loop…u8   Database is empty. Performing initial background scan…zDatabase contains z records. Ready to monitor.is_discord_enabledget_discord_botzHWaiting for Discord bot to connect and be ready before starting scans...<   z-Discord bot is ready. Starting scraper scans.g      ?zWWarning: Discord bot is still not ready after 30 seconds. Proceeding with scans anyway.rR   Nr`   z8] Compare API unavailable. Skipping cycle (fail-closed).r"   r%   r&   z] Scan complete. Active:  | Removed:  | Scanned: ,Initial scan complete. Database seeded with 
 products.Fz+] Failed to retrieve products. Retrying in u   s…z] Unexpected error: z Scraper background loop stopped.)r   r,   r-   r.   r/   r   rO   r   r   backend.notifier.discord_botrq   rr   rangeis_readyrW   rX   rF   r*   r	   r   r
   r1   rg   r2   r   POLL_INTERVALr3   )r]   r6   r,   db_countr#   r^   rq   rr   _botr7   r$   r8   r9   r(   r)   r:   s                    r;   run_scraper_loopr      s   
 D67??$Q'

q=LE	23GH!(+FGH{{x('1 ;;T[\2Y%'3<<>HI

3  no!
4	;)@)B&,'0:H'wug-efg6N$)5)D)573m ;(-9TVZVaVa 7u=02)%)[[]F"NN+[\+1??+<Q+?L"NN+[\,2OO,=a,@M JJLwug-F|nT`an`oo{|  AI  }J  |K  L  M' #OPST\P]^h!ij#';; 23x= A ;CM J+0L75')TU]UkUkTllpqra l x--.A"JJqM /s "| 		+,w 	

L !JJLM d  	;75'!5aS9::	;sV   4L
 *M  0B	L4:ALB!L43M  
LL11L44L=9M   	M$	MM$c            	      
   ddl m} m}  |        st        d       y t        d        |       sy fd}t	        j
                         }t	        j                  |       	 |j                   |              	 t	        j                  |      }|D ]  }|j                           |r$|j                  t	        j                  |ddi       |j                          y # t        $ r}t        d|        Y d }~d }~ww xY w# t        $ r Y y w xY w# 	 t	        j                  |      }|D ]  }|j                           |r$|j                  t	        j                  |ddi       |j                          w # t        $ r Y w w xY wxY w)	Nr   rp   zA[Discord] Discord bot is disabled or configuration is incomplete.z6[Discord] Starting Discord bot in background thread...c                     K   	 j                  t        j                         d {    y 7 # t        $ r} t	        d|         Y d } ~ y d } ~ ww xY ww)Nz[Discord] Bot execution error: )rK   r   DISCORD_BOT_TOKENr3   r   )r:   r~   s    r;   	start_botz"run_discord_bot.<locals>.start_botS  sF     	8))H66777 	821#677	8s6   A"/ -/ A/ 	AAAAAz[Discord] Event loop error: return_exceptionsT)rx   rq   rr   r   asyncionew_event_loopset_event_looprun_until_completer3   	all_taskscancelgatherr/   )rq   rr   r   loopr:   pendingtaskr~   s          @r;   run_discord_botr   E  sW    PQ	AB

C8 !!#D4 	,	''-G  ''(XSW(XYJJL  1+A3/001  			''-G  ''(XSW(XYJJL 		sb   !C 8A"C? 	C<$C72D 7C<<D ?	D
DFA"E32F3	E?<F>E??Fc            
      $   t        j                   t         j                  t               t        j                   t         j                  t               t	        j
                  d      } | j                  ddd       | j                  ddd       | j                  d	dd
       | j                         }t                ddl	m
} |j                          ddlm}m}  ||j                        }t!               }	 |j#                         }|j%                  d       |j'                         d   }|j)                          |dk(  }	|j*                  rt-        d       	 t/               \  }
}|
 t-        d       t1        j2                  d       t5        |
      }
|
 t-        d       t1        j2                  d       t7        |
|	xs |j8                  |      \  }}t;        |||	xs |j8                  |j8                         t!               }	 |j#                         }|j%                  d       |j'                         d   }|j%                  d       |j'                         d   }|j)                          t-        d| d| dt=        |
              |	rMt-        dt=        |
       d       |j8                  sMt?        t=        |
             tA        t=        |
             n$|j8                  stC        ||       tE        ||       t-        d       y tI        jJ                  tL        jN                  d       d } |       r+tQ        jR                  tT        d       }|jW                          tQ        jR                  tX        |fd!      }|jW                          d"tL        jZ                  f}t]        |t^              a0t-        d#tL        jZ                          	 t`        jc                          da3t`        ji                          t-        d%       |jk                  d&'        |       r|rtm        |d(      rtm        |jn                  d)      rj|jn                  jq                         rPt-        d*       dd l9}tu        jv                  |j)                         |jn                        }	 |jy                  d&'       t-        d/       t1        j2                  d       y # |j)                          w xY w# |j)                          w xY w# tF        $ r.}t-        d|        t1        j2                  d       Y d }~*d }~ww xY w# td        $ r t-        d$       Y dw xY w# tt        jz                  |j|                  jz                  f$ r t-        d+       Y tt        j~                  |j|                  j~                  t~        f$ r t-        d,       Y tF        $ r/}t-        d-t        |      j                   d.|        Y d }~Cd }~ww xY w# da3t`        ji                          t-        d%       |jk                  d&'        |       r;|r8tm        |d(      r+tm        |jn                  d)      r|jn                  jq                         rt-        d*       dd l9}tu        jv                  |j)                         |jn                        }	 |jy                  d&'       n# tt        jz                  |j|                  jz                  f$ r t-        d+       Y nstt        j~                  |j|                  j~                  t~        f$ r t-        d,       Y n9tF        $ r.}t-        d-t        |      j                   d.|        Y d }~nd }~ww xY wt-        d/       t1        j2                  d       w xY w)0NzTrackFurb India Laptop Tracker)descriptionz--once
store_truez=Run a single sync scan and exit instead of running in a loop.)actionhelpz--silentzXRun silently without sending any push notifications (use with --once or on initial run).z--sync-commandsz%Sync Discord slash commands globally.r   )catalog_state)init_discord_botrq   )sync_commandsr!   u   Running single scan (--once)…z0Failed to retrieve products. Exiting with error.rR   z<Compare API unavailable. Aborting single scan (fail-closed).r"   r%   r&   zScan complete. Active: rt   ru   rv   rw   z%Unexpected error during single scan: zSingle scan complete. Exiting.T)exist_okrC   r\    z4TrackFurb India tracker serving at http://localhost:z.KeyboardInterrupt received. Stopping server...z9Server stopped. Waiting for background thread shutdown...   )timeoutr   
is_runningz!Closing Discord bot connection...zDiscord bot connection closed.z4Timeout waiting for Discord bot connection to close.zError closing Discord bot: z: zStopped.)BsignalSIGTERMrM   SIGINTargparseArgumentParseradd_argument
parse_argsr   backend.scraper.catalog_stater   init_from_dbrx   r   rq   r   r   r,   r-   r.   r/   oncer   r	   sysexitr   r
   rO   rZ   r2   r   r   r   r   r3   osmakedirsr   
STATIC_DIRrH   rI   r   rK   r   PORTri   r   rG   serve_foreverKeyboardInterruptrF   server_closejoinhasattrr   r   concurrent.futuresr   run_coroutine_threadsafere   CancelledErrorfuturesTimeoutErrortyperj   )parserr]   r   r   rq   r~   r6   r,   r|   r#   r7   r$   r8   r9   r(   r)   r:   discord_threadscraper_threadserver_address
concurrentfuts                         r;   mainr   p  s   
MM&.."23
MM&--!12$$1QRF
L  
 g  
 4  
 D ;  R
););
<CD67??$Q'

q=L yy./*	%<%>"HlGH(2HST*B)8T[[)+'L- '|]LD_TXT_T_aealalm$&DOP%03OP & 1! 4

*<.]OS_`cdl`m_nopCCM?R\]^{{&s8}5/H>{{,\=I5lMR
 	-. KK##d3 N"))N %%-=TGTXYN (--(N0DEE?	OP
 #HIA& SWS&-A,/CHH4G4G4I45%22399;ICL

1
% 	ZA 	

D 

  	8<=HHQKK	6  ?=>? **J,>,>,M,MN 756((**<*<*I*I<X MKL L2473C3C2DBqcJKKL #HIA& SWS&-A,/CHH4G4G4I45%22399;ICL

1
%**J,>,>,M,MN 756((**<*<*I*I<X MKL L2473C3C2DBqcJKKLZs   94S B+S7 AS" B S7 T1 U S"S44S7 7	T. #T))T.1U	W7 U		W7 4W48W4=W4$W//W47B:^2[^4]+9^;8]+3^5]+=$]&!^&]++$^)r   N)Cr   r   rW   r   r   rH   socketserverhttp.serverhttpr   typingr   r   backend.configr   backend.logging_configr   backend.db.connectionr   backend.db.migrationsr   backend.scraper.apir	   backend.scraper.processorr
   backend.scraper.enrichmentr   r   r   backend.scraper.psrefr   backend.notifier.ntfyr   r   backend.server.handlerr   backend.notifier.telegramr   r   r   r   backend.notifier.pushr   r   r   rF   Lockr*   rd   rG   dictr<   r@   intrM   listboolrZ   r1   rg   ThreadingMixInserver
HTTPServerri   r   r   r   rn   rN   r;   <module>r      sO   	 
         # ' 3 0 7 > \ \ 6 N 7    y~~$inn& 6t 6pt &ES ED E$(T $($ $(VZ $(dh $(mq $(N'+;?IM	07# 7$ 7@,55t{{7M7M f-P%VGrN   