# Code Review: job-scheduler

A comprehensive review of the job-scheduler Qt6/C++20 application, covering bugs, correctness issues, and potential improvements.

---

## Bugs (incorrect behavior)

- [x] **BUG 1 — Cron schedule semantics: day-of-month AND day-of-week treated as both-required instead of either-or** (`src/CronTime.cpp:285`) **— FIXED (`da95e87`)**
  ```cpp
  } else if (!day.at(tm.date().day() - 1) || !week.at(tm.date().dayOfWeek())) {
  ```
  Standard cron treats day-of-month and day-of-week as OR: a job runs on the 15th OR every Friday (`0 0 15 * 5`). This code uses `||`, which skips the day unless BOTH fields match — effectively treating them as AND. A job set for "15th OR every Friday" would only run on Fridays that fall on the 15th.
  **Fix:** Change `||` to `&&` so the day is only skipped when NEITHER field matches.

  **Fix applied:** A plain `||`→`&&` swap is incorrect — it breaks the wildcard cases (e.g. `* * 15 * *` would never match). The actual standard-cron rule was implemented instead: OR only when *both* fields are restricted, otherwise AND (a `*` field always matches, so AND collapses to the single restricted field). The wildcard checks (`isFill`) are hoisted out of the iteration loop as `bothDayFieldsRestricted`.

- [x] **BUG 3 — Integer division in `ExecuteView::scrollTo` makes it a no-op** (`src/ExecuteView.cpp:55`) — **FIXED**
  ```cpp
  double step = 1 / rect.height();  // INTEGER division — always 0 when height > 1
  ```
  Compare with `CronView::scrollTo` (`src/CronView.cpp:194`):
  ```cpp
  double step = 1.0 / rect.height();  // FLOAT division — correct
  ```
  The missing `.0` means `step` is always `0.0` when `rect.height() > 1`, so `rect.top() * 0.0 = 0` and the custom scroll logic does nothing. The `+ 5` hack at line 61 hints that someone noticed the scroll wasn't working.
  **Fix:** Change `1 / rect.height()` to `1.0 / rect.height()`.

- [x] **BUG 5 — `Clib::uName()` / `uHome()` / `uShell()` null-pointer dereference** (`src/Clib.h:24-35`) — **FIXED**
  ```cpp
  return getpwuid(uId())->pw_name;
  ```
  `getpwuid()` returns `nullptr` when the UID is not found in the password database (LDAP/NIS transient failure, container environments, unusual system configs). Dereferencing `nullptr->pw_name` is an unconditional crash. Same issue on lines 28 and 34.
  **Fix:** Check the return value of `getpwuid()` before dereferencing.

- [x] **BUG 7 — `CronTime::getNextTime` may waste CPU for impossible schedules** (`src/CronTime.cpp:278-279`) — **FIXED**
  The 10,000-iteration cap prevents a true infinite loop, but a schedule like `0 0 30 2 *` (February 30th) will spin through all iterations and silently return an invalid `QDateTime`. The caller (`ExecuteList::dataChanged`) treats this as "No matching schedule", which is acceptable UI-wise but wastes CPU (10,000 iterations per impossible job).
  **Consider:** Early-exit when the month doesn't contain the requested day. Or document this as a known limitation.

  **Fix applied:** Added an early-exit before the loop. When day-of-week is unrestricted (so OR semantics can't rescue the match via a weekday), the code checks whether any selected day-of-month fits within any selected month's maximum length (using a per-month max-days table, Feb=29). If none fits — e.g. Feb 30th, Apr 31st — it returns an invalid `QDateTime` immediately instead of iterating 10,000 times.

## Correctness / Robustness

- [x] **ISSUE 1 — `stderr` emptiness check is fragile** (`src/Crontab.cpp:62`, `:129`) — **FIXED**
  ```cpp
  if ((p.exitCode() != 0) || !err.isEmpty()) {
  ```
  Some `crontab` implementations write warnings or informational messages to stderr even on success. Relying on `!err.isEmpty()` as a failure indicator produces false positives. Also the error messages say "crontab update error" in the **get** function (`getCrontab` line 62), which is misleading.
  **Fix:** Base the failure check on `p.exitCode()` alone. Keep stderr for the error message.

- [x] **ISSUE 2 — `setup()` hard-codes skipping 3 header lines** (`src/Crontab.cpp:177-181`) — **FIXED**
  ```cpp
  if (slist.at(0).contains(QLatin1String("# DO NOT EDIT THIS FILE"))) {
      slist.removeFirst();
      slist.removeFirst();
      slist.removeFirst();
  }
  ```
  This assumes the crontab header is exactly 3 lines long. Different cron implementations or locales might produce a different header format (some add blank lines, copyright notices, or different timestamp formats). The `.contains()` check only verifies the first line.
  **Fix:** Remove lines while they start with `#` instead of hard-coding a count.

- [x] **ISSUE 3 — `TCommandEdit` timer runs forever** (`src/TCommandEdit.cpp:94`) — **FIXED**
  ```cpp
  timer.start(1min);
  ```
  The `1min` timer fires every 60 seconds for the entire application lifetime, calling `resetExeTime()` which recalculates next execution times. This is wasted work when no command is being edited (the widget is disabled/hidden). It should be stopped in `changeCurrent` when `cmnd == nullptr` and restarted when a command is selected.

- [x] **ISSUE 5 — `TimeDialog` shows "Time format error" then resets to `* * * * *`** (`src/TimeDialog.cpp:178-185`) — **FIXED**
  When given an invalid initial `inTime`, the dialog shows "Time format error" in the text field but silently resets `outTime` to `* * * * *`. If the user clicks OK without noticing, the job silently gets a different schedule. The dialog should either disable OK or revert `timeEdit` to the original text and show a warning.

- [x] **ISSUE 6 — `TimeButton::buttonToggled` uses hardcoded palette colors** (`src/TimeDialog.cpp:47-55`) — **FIXED**
  The manual palette manipulation (`setPalette`) overrides the system theme/style. On dark themes, the hardcoded light colors and dark text may be invisible or jarring. Qt stylesheets or a `QPalette::Highlight` role would be more portable.

## Code Quality / Maintainability

- [x] **ISSUE 9 — Massive duplicated logic in `TimeDialog` button handlers** (`src/TimeDialog.cpp:283-424`) — **FIXED**
  `minuteButtonClicked`, `hourButtonClicked`, `dayButtonClicked`, `monthButtonClicked`, `weekButtonClicked` are nearly identical (each differs only by which group/bitarray they operate on). This is ~140 lines of copy-paste code. A single templated or overloaded handler would be ~20 lines.

- [x] **ISSUE 10 — Duplicated comparators in `ExecuteModel`** (`src/ExecuteModel.cpp:84-115`) — **FIXED**
  Eight nearly identical comparison functions (`ltExeTime`, `ltCronTime`, `ltUser`, `ltCommand`, `gtExeTime`, `gtCronTime`, `gtUser`, `gtCommand`). These can be replaced with a single generic comparator or a lambda generator.

- [x] **ISSUE 11 — Dead code: unused global `starting_home`** (`src/main.cpp:19`) — **FIXED**
  ```cpp
  const extern QString starting_home = qEnvironmentVariable("HOME");
  ```
  This is declared as `extern` but never referenced anywhere else in the codebase. It also stores a `QString` as a global, which may be destroyed before other static objects (`static destruction order fiasco`).

- [x] **ISSUE 12 — Commented-out code blocks throughout** — **FIXED**
  Several files contain large blocks of commented-out code (`CronView.cpp:42, :83-84`, `CronModel.cpp:182-183, :288-310, :313`, `Crontab.cpp:209-210`, `TimeDialog.cpp:424-435`, `ExecuteList.cpp:55`). Dead code makes navigation harder and suggests incomplete refactoring.

- [x] **ISSUE 13 — `CronTime::toString` uses extra parentheses** (`src/CronTime.cpp:211`) — **FIXED**
  ```cpp
  return ("*");
  ```
  Minor style nit — the parentheses are unnecessary.

- [x] **ISSUE 14 — `Model::Data` enum used inconsistently** (`src/CronModel.h:31`) — **FIXED**
  The `Data` enum has `Time, User, Command` but `columnCount` returns 4. Column 3 is never displayed. Either reduce `columnCount` to 3 or add a header for column 3 (or a comment explaining the mystery 4th column).

## Build / CMake

- [x] **ISSUE 15 — `constants.h` not in `HEADERS` list** (`CMakeLists.txt:124-142`) — **FIXED**
  `src/constants.h` is not listed in `HEADERS` but is included by several `.cpp` files. With `CMAKE_AUTOMOC ON`, non-moc headers should still be listed for IDE integration and dependency tracking.

- [x] **ISSUE 16 — `-Werror` in production CMakeLists** (`CMakeLists.txt:176`) — **WON'T FIX**
  `-Werror` turns all warnings into errors. While great for development, this can break builds with newer compiler versions that add new warnings. Consider adding `-Werror` only in CI/debug builds, or curating specific `-Werror=` flags instead of the catch-all.

## Minor / Cosmetic

- [x] **ISSUE 17 — `tommorow` typo** (`src/TCommandEdit.cpp:135`) — **FIXED**
  ```cpp
  QDate tommorow = today.addDays(1);
  ```
  Should be `tomorrow`.

- [x] **ISSUE 19 — Inconsistent `override` on `CronModel::getCrontab` and `getTCommand** — **WON'T FIX**
  These are declared without `override` in the header despite overriding nothing (they're own methods). No issue here, but the convention in the codebase is inconsistent.

---

**Legend:** `[ ]` = unchecked item (review finding). Items are ordered by severity, not file order.
