From 2ab0408cbe380c2fc560f1f3dcb9890998e86f0c Mon Sep 17 00:00:00 2001 From: NguyenND Date: Thu, 23 Apr 2026 15:53:29 -0400 Subject: [PATCH] 04/23 Fixed bugs --- Website_Checker_User_Manual.docx | Bin 21885 -> 0 bytes models.py | 71 ++++++++++++++++++++++++++----- utils/scheduler.py | 43 ++++++++++++++++++- views/admin_websites_view.py | 7 ++- views/ai_summary_view.py | 17 +++++++- 5 files changed, 123 insertions(+), 15 deletions(-) delete mode 100644 Website_Checker_User_Manual.docx diff --git a/Website_Checker_User_Manual.docx b/Website_Checker_User_Manual.docx deleted file mode 100644 index a0aff8c1bd6d0eade993c4d44b3e3255d6e942ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21885 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 0 + AND ( + w.visibility = 'all' + OR EXISTS ( + SELECT 1 FROM website_users wu + WHERE wu.website_id = w.id AND wu.user_id = %s + ) + ) AND ( w.check_type = 'daily' OR ( @@ -664,7 +703,7 @@ def get_today_checks(user_id: int): GROUP BY w.id, sc.id, sc.checked_at, sc.user_note, sc.user_id ORDER BY sort_order, w.name """, - (user_id, user_id, user_id) + (user_id, user_id, user_id, user_id) ) else: # Legacy fallback: show active websites, filtered by visibility @@ -861,17 +900,27 @@ def get_unchecked_report(target_date=None, user_id=None): conn = get_connection() cur = conn.cursor(dictionary=True) - date_val = str(target_date) if target_date else None - user_filter = "AND u.id = %s" if user_id else "" - params_inner = [date_val or "CURDATE()"] - if user_id: - params_inner.append(user_id) + + # Use a bind param only when a specific date is provided. + # When no date is given, embed CURDATE() directly in SQL so MySQL + # evaluates it as a function — passing "CURDATE()" as a %s bind + # parameter treats it as a literal string, not a SQL function, and + # causes the NOT EXISTS filter to match nothing (returns 0 rows). + if target_date: + date_val = str(target_date) + date_expr = "%s" + date_params = [date_val] + else: + date_expr = "CURDATE()" + date_params = [] + + params = date_params + ([user_id] if user_id else []) + date_params cur.execute( f""" SELECT - %s AS check_date, + {date_expr} AS check_date, u.username, COALESCE(u.full_name, u.username) AS full_name, w.name AS website_name, @@ -887,11 +936,11 @@ def get_unchecked_report(target_date=None, user_id=None): SELECT 1 FROM shift_checks sc WHERE sc.website_id = w.id AND sc.user_id = u.id - AND DATE(sc.checked_at) = %s + AND DATE(sc.checked_at) = {date_expr} ) ORDER BY u.username, w.name """, - ([date_val or "CURDATE()"] + ([user_id] if user_id else []) + [date_val or "CURDATE()"]) + params ) rows = cur.fetchall() cur.close() diff --git a/utils/scheduler.py b/utils/scheduler.py index af40af1..c8cabff 100644 --- a/utils/scheduler.py +++ b/utils/scheduler.py @@ -175,10 +175,38 @@ def _send_report(cfg: dict): logger.error(f"Failed to send daily report email: {e}") +def _get_last_sent_date() -> "datetime.date | None": + """Read the last-sent date from config.ini [email] last_sent_date key.""" + cfg = configparser.ConfigParser() + if not os.path.exists(CONFIG_FILE): + return None + cfg.read(CONFIG_FILE, encoding="utf-8") + raw = cfg.get("email", "last_sent_date", fallback="") + if not raw: + return None + try: + return datetime.date.fromisoformat(raw) + except ValueError: + return None + + +def _set_last_sent_date(d: "datetime.date"): + """Persist the last-sent date to config.ini [email] last_sent_date key.""" + cfg = configparser.ConfigParser() + if os.path.exists(CONFIG_FILE): + cfg.read(CONFIG_FILE, encoding="utf-8") + if "email" not in cfg: + cfg["email"] = {} + cfg["email"]["last_sent_date"] = d.isoformat() + with open(CONFIG_FILE, "w", encoding="utf-8") as fh: + cfg.write(fh) + + # ─── Scheduler loop ─────────────────────────────────────────────────────────── def _scheduler_loop(): - last_sent_date = None + # Seed from persisted value so a restart after send_time does not re-send. + last_sent_date = _get_last_sent_date() while not _stop_event.is_set(): _stop_event.wait(60) # sleep 60 seconds between checks @@ -200,6 +228,7 @@ def _scheduler_loop(): if (now.hour > send_h or (now.hour == send_h and now.minute >= send_m)): if last_sent_date != today: last_sent_date = today + _set_last_sent_date(today) logger.info(f"Scheduler: sending daily report at {now.strftime('%H:%M')}.") _send_report(cfg) @@ -207,7 +236,17 @@ def _scheduler_loop(): def start(): """Start the background scheduler thread. Call once after successful login.""" global _scheduler_thread, _stop_event - _stop_event.clear() + # Guard against double-start: if a thread is already alive (e.g. admin + # logs out and back in), stop it cleanly before spawning a new one. + # Without this guard, _stop_event.clear() would unblock the sleeping + # thread while a second thread also starts, resulting in two scheduler + # threads firing simultaneously and potentially sending duplicate emails. + if _scheduler_thread is not None and _scheduler_thread.is_alive(): + logger.info("Email scheduler already running - stopping before restart.") + _stop_event.set() + _scheduler_thread.join(timeout=5) + # Create a fresh Event so there is no residual set-state from a prior stop() + _stop_event = threading.Event() _scheduler_thread = threading.Thread(target=_scheduler_loop, name="EmailScheduler", daemon=True) _scheduler_thread.start() diff --git a/views/admin_websites_view.py b/views/admin_websites_view.py index 5fa8c7f..ea9eaac 100644 --- a/views/admin_websites_view.py +++ b/views/admin_websites_view.py @@ -352,8 +352,13 @@ class WebsiteDialog(tk.Toplevel): def _on_visibility_change(self): """Show or hide the user assignment panel based on visibility selection.""" if self.visibility_var.get() == "assigned": + # Use before=self._cred_body (not self.creds_container) because + # pack's `before` argument requires the reference widget to share + # the same parent. _user_assign_frame and _cred_body are both + # children of self.inner; creds_container is a child of _cred_body, + # so referencing it across that boundary raises a TclError. self._user_assign_frame.pack(fill="x", padx=24, pady=(0, 4), - before=self.creds_container) + before=self._cred_body) else: self._user_assign_frame.pack_forget() diff --git a/views/ai_summary_view.py b/views/ai_summary_view.py index c94486b..d48f73c 100644 --- a/views/ai_summary_view.py +++ b/views/ai_summary_view.py @@ -851,17 +851,32 @@ def _read_doc(path: str) -> str: def _read_excel(path: str) -> str: + """ + Extract text from an Excel file via openpyxl (read-only mode). + Iteration stops as soon as accumulated content exceeds MAX_CHARS_PER_FILE + to avoid loading the entire workbook into memory for very large files. + The AI layer will truncate to MAX_CHARS_PER_FILE anyway, so there is no + value in continuing past that point. + """ + MAX_CHARS_PER_FILE = 14_000 # mirror the constant in AiSummaryView._run_ai try: import openpyxl wb = openpyxl.load_workbook(path, read_only=True, data_only=True) lines = [] + total_chars = 0 for sheet in wb.worksheets: - lines.append(f"[Sheet: {sheet.title}]") + header = f"[Sheet: {sheet.title}]" + lines.append(header) + total_chars += len(header) + 1 for row in sheet.iter_rows(values_only=True): row_str = "\t".join( str(v) if v is not None else "" for v in row) if row_str.strip(): lines.append(row_str) + total_chars += len(row_str) + 1 + if total_chars >= MAX_CHARS_PER_FILE: + lines.append("[... content truncated to fit token limit ...]") + return "\n".join(lines) return "\n".join(lines) except ImportError: raise ImportError(