Chore/refactor#45
Conversation
Add option to only do daily tasks Add a Stop button to properly shutdown every Edge instances
Ports release-prep content from main (bc88a5a + da7dd64 USER_GUIDE bits) onto the refactor layout. Version bumped in AutoRewarder.iss, gui/index.html and src/config.py. README/USER_GUIDE refreshed with v3.3 sections (tray, daily-tasks-only, advanced scheduling), keeping the refactored src/ directory tree.
Ports ff2a5b2 (advanced scheduling) + cea6f3f (schedule control guard) onto the refactored api.py. Drip-feeds queries across the configured duration in ~10-minute batches with jittered sleeps, respects the Stop event, and warns when advancedScheduling is enabled but the master Schedule toggle is off. The GUI plumbing (account_meta.get_schedule / set_schedule, modal toggles) was already in place on refactor — only the backend was missing.
Ports dc0f47f + 1d9ba94 + b9b1057 (combined). Closing the main window now hides it to a system tray (pystray + Pillow). Tray menu: Open (default — restore window), Exit (force destroy). Falls back gracefully if pystray/PIL aren't installed. Spec file: adds pystray/_win32 and PIL/Image to hiddenimports so the PyInstaller bundle includes them.
Adds module-level docstrings to every src/ module and expands method
docstrings with Args/Returns/Raises sections. Where files were split by
the refactor (settings_manager → accounts/{meta,settings}, daily_set →
dailytasks/{runner,card,card_js}), the docstrings landed on the
corresponding new class/function.
The refactor commit had dropped .pre-commit-config.yaml and requirements-dev.txt (added on main via the daily-tasks PR). Restored them verbatim from main so local hooks and CI mypy match again. Also ported main's lint.yml — it now installs from requirements-dev.txt instead of pinning tool versions inline. Added linter/venv cache patterns to .gitignore.
Replaces the previous fire-on-login autostart (HKCU Run on Windows,
~/.config/autostart/*.desktop on Linux) with a daily wall-clock
trigger at AUTOSTART_TIME (09:00 by default):
* Windows: schtasks /Create /SC DAILY /ST 09:00 /F
* Linux: systemd --user .service + .timer with OnCalendar=*-*-*
09:00:00 and Persistent=true (catches up after suspend/off)
Adds _cleanup_legacy_autostart() so users transitioning from the old
mechanism don't end up with both fires registered. is_autostart_enabled
now queries the scheduler directly (schtasks /Query, systemctl --user
is-enabled) instead of poking the registry / scanning files.
Solves the "always-logged-in machines never re-fire" failure mode of
the previous design.
(Sourced from the fix/daily WIP stash, ported manually onto the
refactored layout to preserve all docstrings and the scheduling
backend untouched.)
Pre-commit hooks (end-of-file-fixer, trailing-whitespace, mixed-line-ending) auto-fixed pre-existing inconsistencies across docs, configs and assets. Also adds a .flake8 config with a per-file E501 ignore for src/dailytasks/card_js.py — that module embeds JS regex literals as Python triple-quoted strings; the regex bodies must stay on one line for the JS engine to parse them.
Mirrors the _migrate_legacy_global_schedule pattern: on every app launch, if a legacy HKCU Run entry (Windows) or .desktop autostart file (Linux) exists but the new daily scheduler isn't set up yet, transparently switch over to schtasks /SC DAILY (Windows) or systemd --user timer (Linux). Users upgrading don't have to manually toggle Autostart off-then-on to stop the visible GUI window from popping at every login. No-op when the new scheduler is already active. Failures are silently swallowed — a stale legacy entry is not worth crashing app startup.
Port of upstream 594eb14. The trailing "Advanced scheduled run complete." log fired unconditionally, even when the run was stopped or ran out of queries early. Now logs "Advanced schedule completed!" only when the loop exited cleanly with both counters at 0 and no Stop pressed.
Brings origin/main up to v3.3 (594eb14) into the refactored layout so the PR can fast-forward. All substantive content from main was already ported on this branch — conflicts were the structural ones git can't auto-detect: * GUI/ vs gui/ → keep gui/ (lowercase) * src/daily_set.py → keep deleted (split into src/dailytasks/*) * src/settings_manager → keep split into src/accounts/{meta,settings}.py * src/api.py → keep refactor's autostart+scheduling rewrite Screenshots: take main's v3.3 set (main_window_3.3.png/.gif), drop the older main_2.png / main_demo.gif.
The previous design used a single OS-level scheduled task firing at
a hardcoded 09:00 that then iterated every account. This switches to
one scheduled task per account, each with its own configurable HH:MM
trigger.
* schedule.run_time (HH:MM, defaults to 09:00) added to per-account
meta.json; validated by _normalize_run_time on both ends
* Windows: schtasks /Create /TN AutoRewarder.<account_id>
/TR "...exe --headless --account <id>" /SC DAILY /ST HH:MM /F
* Linux: systemd --user autorewarder-<account_id>.{service,timer}
with OnCalendar=*-*-* HH:MM:00 and Persistent=true
* _sync_account_autostart(id) creates / updates / removes one
account's task based on its schedule.enabled and run_time
* _set_autostart_registry now persists autoStartUp in global settings
and syncs every account; off-toggle removes all per-account tasks
* set_schedule re-syncs the account's OS task on every save
* delete_account tears down the OS task before removing files
* _migrate_legacy_autostart now also detects the v3.3 single-task
daily scheduler and migrates it to per-account
* _cleanup_legacy_autostart also strips the single-task remnants
GUI: each schedule card now has a "Daily run time" field (HTML5
time input). Live summary shows the time. Save path validates
HH:MM and includes run_time in the payload.
Users staggering several accounts (e.g. Alice 09:00, Bob 10:30) no
longer see concurrent Edge instances fighting for resources.
Two bugs landed in the previous per-account autostart commit: 1. _migrate_legacy_autostart short-circuited as soon as autoStartUp=True was persisted. If the very first migration's schtasks delete had silently failed (returncode not checked, no log), every subsequent launch read autoStartUp=True and returned immediately — the stale legacy single-task `AutoRewarder` survived forever alongside the new per-account ones, firing concurrently. 2. _cleanup_legacy_autostart wrapped schtasks /Delete in capture_output=True without inspecting the result, so a failed delete left no trace in the log. Fix: * Split detection into _detect_legacy_autostart(); call it on every startup. If a stale legacy entry exists AND we're already on the per-account model (autoStartUp=True), just run cleanup without touching per-account tasks. If autoStartUp=False, do the full migration as before. * _cleanup_legacy_autostart now queries before deleting on Windows (so we only log delete failures when the task actually existed), and logs both success and failure paths for schtasks + systemd + .desktop. Users hitting permission issues now have a "[WARNING] Could not delete legacy task..." line in the log to point at. Users currently in the broken state (both tasks present) will see the stale one removed on the next app launch.
New global setting `close_to_tray` (default True for backwards compat with the v3.3 tray rollout) lets users opt out of the close-to-tray behavior. When OFF, the X button quits the app like a normal window and the tray icon is never installed in the first place. The setting is read once at startup, mirroring how `hide_browser` and `autoStartUp` are loaded — flipping the toggle in Settings shows a log line confirming the change with a "Restart to apply" hint. * src/accounts/settings.py: defaults + set_close_to_tray * src/api.py: get_close_to_tray / set_close_to_tray exposed to JS * AutoRewarder.py: read at startup, gate _install_tray on the value * gui/index.html: new Close-to-tray toggle in the General section * gui/script.js: load on Settings open, persist on Save
When the user runs from source (sys.frozen=False) on Windows, the registered schtasks command pointed at python.exe — the console variant. Every time the task fired, Windows allocated a console window for that process, producing a brief Python terminal popup that's exactly what the daily scheduler was supposed to prevent. Fix: _autostart_command prefers pythonw.exe (same interpreter, no console) when it exists next to sys.executable on Windows. The bundled exe path is unaffected — PyInstaller's console=False already guarantees silence there. Existing users on autoStartUp=True have tasks still pointing at python.exe. Introduce `autostart_schema_version` (current=1) in global_settings so _migrate_legacy_autostart now also handles a "refresh tasks under current schema" path: at next app launch, every per-account task is re-registered with the new command. No user action required.
|
Hi @JeromeM, thanks for this PR. I'll get to testing and reviewing this properly as soon as I have some free time. Really appreciate your work on this. |
|
As always, take your time and focus with your exams :) |
There was a problem hiding this comment.
Pull request overview
This PR performs a large internal refactor of src/ into feature-based subpackages while also redesigning OS-level autostart from “fire-on-login” to per-account daily scheduled tasks (each account has its own HH:MM trigger time). It also introduces a global Close-to-tray toggle and adds/updates GUI surfaces to configure these behaviors.
Changes:
- Reorganized the Python backend into
accounts/,emulator/,search/, anddailytasks/subpackages with stable exports via each package’s__init__.py. - Reworked autostart/scheduling to create and manage one scheduled task per account (Windows Task Scheduler / Linux systemd user timers), including legacy artifact migration/cleanup.
- Updated the GUI to support a per-account “Daily run time” field and a global “Close to tray” setting; added a new history window.
Reviewed changes
Copilot reviewed 21 out of 30 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/search/history.py | Adds per-account history storage manager with backup-on-corruption handling. |
| src/search/engine.py | Updates imports to align with the new package layout. |
| src/search/init.py | Exposes SearchEngine and HistoryManager as the src.search public API. |
| src/emulator/human.py | Adds/refactors human-like input simulation (mouse + touch) for Selenium. |
| src/emulator/edge_policy.py | Adds Windows-only Edge policy helper module for sign-in behavior. |
| src/emulator/driver.py | Introduces per-account Edge WebDriver setup and mobile emulation support. |
| src/emulator/init.py | Exposes emulator public API (DriverManager, HumanBehavior, edge_policy). |
| src/dailytasks/runner.py | Splits daily tasks orchestration from the old monolith into a focused runner. |
| src/dailytasks/card.py | Adds a RewardsCard abstraction for DOM heuristics + click/tab handling. |
| src/dailytasks/card_js.py | Centralizes JS heuristics and introduces CardStatus enum. |
| src/dailytasks/init.py | Exposes daily-tasks public API (DailySet, RewardsCard, CardStatus). |
| src/daily_set.py | Removes the previous monolithic daily set implementation (moved/split). |
| src/config.py | Updates GUI directory constant to match GUI/ → gui/ rename. |
| src/api.py | Major wiring changes: new packages, per-account scheduling/autostart sync, close-to-tray API. |
| src/accounts/settings.py | Adds global settings persistence (including close_to_tray) under new layout. |
| src/accounts/meta.py | Adds run_time to per-account schedule defaults and adjusts module responsibilities. |
| src/accounts/manager.py | Updates imports to match new package structure; continues account CRUD. |
| src/accounts/init.py | Exposes accounts public API (AccountManager, AccountMetaManager, etc.). |
| README.md | Updates repository structure documentation to reflect the new layout. |
| gui/styles.css | Replaces/introduces consolidated styling for the refactored GUI. |
| gui/settings.js | Adds/refactors account management modal logic under new GUI layout. |
| gui/script.js | Adds “Daily run time” + “Close to tray” wiring and schedule save/validation updates. |
| gui/normalize.css | Adds a CSS reset for consistent styling across WebView contexts. |
| gui/index.html | Adds Close-to-tray toggle in Settings UI and reflects GUI directory rename. |
| gui/history.html | Adds a dedicated execution history window UI. |
| gui/history.css | Adds styling for the new history window. |
| AutoRewarder.spec | Updates PyInstaller datas to bundle gui/ instead of GUI/. |
| AutoRewarder.py | Gates tray installation on the new close_to_tray setting. |
| AutoRewarder_CLI.py | Updates imports for AccountMetaManager after refactor. |
| .flake8 | Adds flake8 config and per-file ignore for embedded JS regex line length. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| current = driver.find_elements(By.CSS_SELECTOR, selector) | ||
| if idx >= len(current): | ||
| self._log( | ||
| f"[WARNING] {section_name} card #{idx + 1} disappeared between " | ||
| f"snapshot and click; skipping." | ||
| ) | ||
| continue | ||
| target_card = current[idx] |
| # Path 1: full migration from legacy to per-account. | ||
| if legacy and not autostartup: | ||
| self._safe_log( | ||
| "Migrating legacy autostart entry to per-account daily tasks..." | ||
| ) | ||
| try: | ||
| self._set_autostart_registry(True) | ||
| except Exception as e: | ||
| self._safe_log(f"[WARNING] Autostart migration failed: {e}") | ||
| else: |
| # OS-level autostart. When True, the app registers a Run entry | ||
| # (Windows registry) or a .desktop autostart file (Linux). | ||
| "autoStartUp": False, |
| @@ -0,0 +1,7 @@ | |||
| [flake8] | |||
| max-line-length = 140 | |||
There was a problem hiding this comment.
Since max-line-length = 140 is now globally defined in .flake8, we can remove the arguments from .pre-commit-config.yaml and lint.yml to keep things DRY
| // this account. Only effective when the global Start-with-Windows | ||
| // toggle is on AND this account's schedule is enabled. | ||
| const timeDefault = (sched.run_time && /^\d{2}:\d{2}$/.test(sched.run_time)) ? sched.run_time : '09:00'; | ||
| body.appendChild(make_form_field('Daily run time', 'time', 'schedule-run-time', timeDefault, {})); |
|
Thanks for the review. I'll look into it next week when I'll be back home. |



Summary
Structural refactor of
src/into feature-based subpackages, alignmentwith
mainv3.3, and a redesign of the OS-level daily autostart:fire-on-login → one scheduled task per account, each with its own
configurable HH:MM trigger time. Also adds a user-controlled
Close-to-tray toggle so the tray behavior is opt-out.
No user-visible behavior change in the GUI beyond the new per-account
"Daily run time" field and the new "Close to tray" toggle — the
reorganization is purely internal, and every v3.3 functional addition
from
mainwas ported as-is.Why
The
src/daily_set.pymonolith (1037 lines) and the loose file layoutat the root of
src/made navigation and maintenance painful. Inparallel, the original "HKCU Run / .desktop" autostart only fired on
login — on a machine left logged in for several days, the daily run
was never triggered, and it always popped a visible GUI window at
every session start. A first iteration moved this to a single
scheduled task at 09:00, but multiple accounts then ran concurrently
and there was no way to stagger them.
What changed
1.
src/reorganization into subpackagessrc/account_manager.pysrc/accounts/manager.pysrc/settings_manager.pysrc/accounts/{meta,settings}.py(split by class)src/daily_set.py(1037 L)src/dailytasks/{runner,card,card_js}.pysrc/driver_manager.pysrc/emulator/driver.pysrc/edge_policy.pysrc/emulator/edge_policy.pysrc/human_behavior.pysrc/emulator/human.pysrc/search_engine.pysrc/search/engine.pysrc/history.pysrc/search/history.pyGUI/gui/(lowercase)Each subpackage exposes a stable API through its
__init__.py. Noduplication, no class renames.
2. Per-account daily-scheduler autostart
Each account now owns its own OS-level scheduled task that fires at
its own configurable wall-clock time:
schtasks /Create /TN AutoRewarder.<account_id> /TR "...exe --headless --account <id>" /SC DAILY /ST HH:MM /F--userautorewarder-<account_id>.{service,timer}with
OnCalendar=*-*-* HH:MM:00andPersistent=true(catches upon missed triggers if the machine was suspended / off)
--headless --account <id>, so thescheduled run goes straight to
AutoRewarder_CLI.main()for thatone account — no GUI window, no other accounts touched.
Per-account state lives in
accounts/<id>/meta.json:{ "schedule": { "enabled": true, "run_time": "09:00", // NEW — HH:MM, validated "queries_pc": 30, ... } }The global "Start with Windows/Linux" toggle remains, but it's now a
master switch: ON syncs every account whose
schedule.enabled = trueto its own task; OFF removes all per-account tasks at once.Toggling a single account's schedule on/off (or changing its
run_time) re-syncs that one task in real time viaset_schedule.Deleting an account tears down its scheduled task before file
deletion.
The fire time is exposed in the GUI as a new "Daily run time" input
on each schedule card (HTML5
<input type="time">).3. Close-to-tray toggle
New global setting
close_to_tray(defaultTrue, preserves the v3.3tray rollout) with a toggle in Settings → General. When OFF, the X
button quits the app like a normal window and the tray icon is never
installed in the first place. Read once at startup, mirroring how
hide_browserandautoStartUpare loaded — the in-app log confirmsthe change with a "Restart to apply" hint.
4. v3.3 alignment (ported from
main)_run_advanced_schedule,_sleep_with_stop) + guard whenadvancedSchedulingis enabledbut the master Schedule toggle is off
graceful fallback if the deps aren't installed
src/fileAutoRewarder.iss,gui/index.html,src/config.py)5. CI & tooling
.pre-commit-config.yaml+requirements-dev.txt.github/workflows/lint.ymlaligned with the versions pinned inrequirements-dev.txt.flake8withper-file-ignoresforsrc/dailytasks/card_js.py(the embedded JS regex literals must stay on a single line)
.gitignoreextended (lint caches + venvs)User migration
No action required. On every launch,
_migrate_legacy_autostartdetects any pre-per-account autostart artifact and either fully
migrates it or just cleans up the stale remnants:
.desktop) from older versions →removed; per-account tasks created for every enabled account
AutoRewardertask at hardcoded09:00) from an intermediate version → removed; per-account tasks
created at each account's
run_time(default 09:00)(e.g. a previous migration's
schtasks /Deletesilently faileddue to permissions) → cleanup retried at every startup, with
[WARNING] Could not delete legacy task '...'logged on failureso the user knows to delete it by hand
Per-account schedules (
accounts/<id>/meta.json) are preserved.Edge profiles, history, and settings remain intact.
To verify after the update:
You should see one entry per enabled account (with the
.<id>/-<id>suffix) and no entry for the unsuffixed legacy name.Test plan
pre-commit run --all-files→ green (black, flake8, mypy,whitespace hooks)
python -c "from src.api import AutoRewarderAPI; AutoRewarderAPI()"→ instantiates without error
per-account schedule editable with the new "Daily run time"
field
icon visible, "Open" restores it, "Exit" kills the process
cleanly
appear, X button quits the app normally
python AutoRewarder.py --headless→ no window,logs land in
background_log.txton one account (1h / 10 qph) → run paced in ~10-minute batches
orphan instances
different times → 2 scheduled tasks appear, each at its own
time
removed
run_timeand save → that account'stask is re-registered at the new time
old task disappears on next launch, per-account tasks appear
the cleanup is retried on every launch and logs a
[WARNING]line with schtasks's error message instead of silently giving up
Notable trade-offs
mainmerge resolves the conflicts inherent to structuralrenames (git doesn't detect
daily_set.py→dailytasks/*as arename). This is intentional and clean on the code side, but
GitHub displays an artificially large changeset, dominated by
moved files.
_set_autostart_registrykeeps its historical name (rather thanbecoming
_set_autostart) so no existing caller in the CLI or intests breaks.
run_timeyet is"09:00"(constantAUTOSTART_TIMEinsrc/api.py).accounts to the same time — by design, since the user chose those
times. The whole point of per-account times is to let them stagger.
the toggle requires a restart to take effect, consistent with how
hide_browserandautoStartUpare loaded. The in-app log makesthis explicit.
Reviewers
Suggested reading order:
src/accounts/__init__.py,src/emulator/__init__.py,src/search/__init__.py,src/dailytasks/__init__.py— publicsurface of the new packages
src/accounts/meta.py— the newrun_timeschedule fieldsrc/accounts/settings.py— the newclose_to_trayglobal settingsrc/api.py— where everything wires together:_normalize_run_time,_windows_task_name,_systemd_unit_base(per-account naming helpers)_sync_account_autostart/_sync_all_autostart(the newsync layer)
set_schedule(re-syncs the OS task after every save)_detect_legacy_autostart/_migrate_legacy_autostart/_cleanup_legacy_autostart(idempotent cleanup with logging)get_close_to_tray/set_close_to_tray(JS-exposed)gui/script.js+gui/index.html— new "Daily run time" and"Close to tray" UI
AutoRewarder.py— tray icon installation gated on the setting*/manager.py,*/runner.pyetc. are essentially the content ofthe old files, resplit by responsibility