# Clock-out Time Calculator — Agent Guide > Last verified against the working tree at version **2.11.2** (`core/version.py`). > This file is written for AI coding agents who need to understand, modify, build, or release the project. When in doubt, prefer the facts in this file over older documentation; this guide was produced by exploring the actual codebase, running the tests, and reading the build scripts. --- ## 1. Project Overview **Clock-out Time Calculator** (Korean: 퇴근시간 계산기) is a Windows desktop productivity application written in Python with PyQt5. It tracks a user's workday, automatically detects clock-in time from Windows Event Log / boot time, counts down to the expected clock-out time in real time, banks overtime in configurable units, manages annual leave, and provides statistics, notifications, Discord integration, and automatic self-updates. - **Primary language:** Python 3.9+ - **GUI framework:** PyQt5 - **Database:** SQLite (`database.db`) with WAL mode and a 5-second busy timeout - **Packaging:** PyInstaller (`main.exe` + `updater.exe`) - **Distribution:** Gitea Releases on a self-hosted instance - **Current version:** `2.11.2` (single source of truth: `core/version.py`) - **Repository:** `kindnick/Clock_out_Time_Calculator` The project is single-file deployable: `main.exe` embeds `updater.exe` and extracts it on first launch, so end users only need `main.exe`. --- ## 2. Technology Stack | Layer | Technology | |-------|------------| | Language | Python 3.9+ | | GUI | PyQt5 ≥ 5.15 | | Charts | matplotlib (QtAgg backend) | | Windows integration | pywin32 (event log), ctypes (screen-lock detection) | | Date / recurrence | python-dateutil | | Notifications | plyer (system toast) + PyQt signals | | Holidays | optional `holidays` package; government API + fixed-date fallback | | Packaging | PyInstaller 2-step build | | Testing | pytest + standalone integration/GUI smoke scripts | | Fonts | Bundled NanumSquare TTF/OTF files; Malgun Gothic fallback | Dependencies are declared in `requirements.txt`: ```text PyQt5>=5.15.0 pywin32>=305 python-dateutil>=2.8.0 matplotlib>=3.4.0 plyer>=2.0.0 holidays>=0.40 ``` Install with: ```bash pip install -r requirements.txt ``` `pywin32` is required for Windows Event Log access and screen-lock detection. The app is therefore Windows-centric; full functionality will not work on other platforms. --- ## 3. Directory Structure and Module Map ```text Clock-out Time Calculator/ ├── main.py # Application entry point / bootstrap ├── updater.py # Standalone update helper process (stdlib only) ├── main.spec # PyInstaller spec for main.exe ├── updater.spec # PyInstaller spec for updater.exe ├── release.ps1 # One-shot release script (PowerShell) ├── requirements.txt # Python dependencies ├── pytest.ini # pytest configuration ├── run_as_admin.bat # Convenience launcher ├── core/ # Business logic and data access │ ├── database.py # SQLite schema, migrations, CRUD │ ├── time_calculator.py # Pure time-math engine │ ├── event_monitor.py # Windows Event Log clock-in detection │ ├── notifier.py # Notification rule engine │ ├── salary.py # Optional pay estimation │ ├── i18n.py # Korean/English translation dictionaries │ ├── settings_keys.py # Setting key constants │ ├── achievements.py # 357 achievement definitions + evaluator │ ├── recurring_leaves.py # Recurring leave pattern expansion │ └── version.py # __version__ single source of truth ├── ui/ # PyQt5 views and widgets │ ├── main_window.py # Central 1 Hz main window │ ├── styles.py # Theme colors and QSS │ ├── dark_components.py # Reusable dark-styled widgets │ ├── icons.py # Icon resource helpers │ ├── i18n_runtime.py # Runtime retranslation registry │ ├── settings_view.py # Settings dialog │ ├── stats_view.py # Weekly/monthly/pattern statistics │ ├── chart_widget.py # matplotlib QtAgg wrapper + fallback │ ├── calendar_view.py # Work-record calendar │ ├── leave_calendar_view.py # Color-coded leave calendar │ ├── break_view.py # Break history dialog │ ├── overtime_view.py # Overtime bank/usage dialog │ ├── leave_view.py # Leave management dialog │ ├── recurring_leave_dialog.py │ ├── schedule_view.py # Schedule view │ ├── clock_in_dialog.py # Manual clock-in dialog │ ├── meal_time_dialog.py # Lunch/dinner start-end input │ ├── past_record_dialog.py # Add past record dialog │ ├── achievements_view.py # Achievements browser │ ├── onboarding_view.py # 5-step first-run wizard │ ├── today_summary.py # Post-clock-out card │ ├── goal_widget.py # Monthly goal progress bars │ ├── mini_widget.py # Always-on-top frameless widget │ ├── help_view.py # 6-tab help dialog │ ├── accessibility.py # Font scale / high-contrast helpers │ └── controllers/ # Thin controllers split from MainWindow │ ├── lock_monitor.py # Screen-lock auto-break / unlock clock-in │ ├── auto_lunch.py # Auto-lunch after 4 hours │ ├── notification_orchestrator.py # 5-min-tick orchestrator │ └── meal_controller.py # Lunch/dinner toggle handling ├── utils/ # Helpers and integrations │ ├── backup.py # Daily SQLite backup with 7-rotation │ ├── lock_detector.py # Win32 screen-lock detection │ ├── discord_webhook.py # Discord embed pushes │ ├── csv_importer.py # CSV import (standard format) │ ├── csv_exporter.py # CSV export │ ├── crash_handler.py # Global excepthook + optional Gitea report │ ├── updater_client.py # Gitea Releases API client │ ├── system_tray.py # System tray menu │ ├── time_format.py # format_hours_minutes helper │ ├── font_loader.py # NanumSquare font loading │ ├── resource_manager.py # PyInstaller _MEIPASS path resolver │ ├── debug_log.py # CLOCKOUT_DEBUG gated logging │ └── holiday_api.py # Korean holiday API client ├── tests/ # pytest unit tests (13 files) ├── font/ # Bundled NanumSquare fonts ├── resources/ # Icons and resource links ├── analysis/ # Currently empty (only __init__.py) ├── build/ # Build staging directory └── dist/ # Built EXEs and release ZIPs ``` > **Note:** `utils/http_api.py` is referenced in older documentation but is **not present** in the current working tree. --- ## 4. How to Build, Run, and Smoke-Test ### Development run ```bash python main.py ``` `main.py` inserts the project root into `sys.path`, bootstraps the database, loads fonts, installs the crash handler, shows onboarding if needed, and launches `MainWindow`. A convenience batch file is provided: ```bash run_as_admin.bat ``` Running as administrator is recommended because Windows Event Log access may be restricted for standard users. ### Module-level smoke tests ```bash python core/event_monitor.py python core/time_calculator.py ``` These run lightweight self-tests when invoked as scripts. ### Production build (manual two-step) ```bash python -m PyInstaller --clean updater.spec mkdir -p build/staging && cp dist/updater.exe build/staging/ python -m PyInstaller --clean main.spec ``` - `updater.spec` builds `dist/updater.exe` (~6 MB, stdlib only, no Qt/matplotlib/win32/holidays). - `main.spec` builds `dist/main.exe` (~78 MB) and embeds `updater.exe` from `build/staging/updater.exe` (falling back to `dist/updater.exe`). - The staging copy is critical: `main.spec --clean` wipes `dist/`, so without `build/staging/updater.exe` the updater would be deleted mid-build. ### Production build (one-shot) ```bash .\release.ps1 v2.11.2 ``` The version argument must match `^v\d+\.\d+\.\d+$` and will be written into `core/version.py`. ### Build gotchas - Kill any running `main.exe` before building or PyInstaller will fail with `PermissionError`. - The `holidays` package is only baked into the EXE if it is installed in the build environment. - Optional code signing is supported via `$env:CODE_SIGN_CERT` (.pfx path) and `$env:CODE_SIGN_PASS`. - `main.spec` lists several `hiddenimports` that are easy to break: `holidays`, `holidays.countries.south_korea`, `win32evtlog`, `win32evtlogutil`, `matplotlib.backends.backend_qtagg`, `matplotlib.backends.backend_qt5agg`, `PyQt5.QtSvg`, `PyQt5.sip`, and `numpy.core._multiarray_tests`. --- ## 5. Testing Strategy The project uses three layers of tests. ### 5.1 Unit tests (pytest) Configuration: `pytest.ini` ```bash python -m pytest tests/ -v --tb=short ``` There are **13 test files** under `tests/`: - `test_time_calculator.py` — clock-out, overtime truncation, holiday overtime, day-type detection - `test_database.py` — settings, migrations, leave calculations, consecutive OT days - `test_csv_importer.py` — CSV parsing, validation, conflict handling - `test_salary.py` — pay estimation and won formatting - `test_recurring_leaves.py` — pattern parsing and expansion - `test_crash_handler.py` — crash log insertion and Gitea reporting (mocked) - `test_discord_webhook.py` — URL validation, payload shape, network errors - `test_holiday_api.py` — API response parsing and error handling - `test_i18n.py` — language switching, missing-key fallback, interpolation - `test_i18n_runtime.py` — runtime retranslation of Qt widgets - `test_overtime_accrual_guard.py` — auto-overtime rollover guard - `test_updater.py` — version parsing, Gitea API URL, update logic `tests/conftest.py` sets `CLOCKOUT_DISABLE_HOLIDAY_SYNC=1` so the background holiday-sync thread does not hold open temporary DB files during test cleanup. **Current status:** `194 passed`. ### 5.2 Integration scenarios ```bash python _integration_test.py ``` A standalone script with a custom `@case` decorator. It runs **53 scenarios** (S1–S52, S52A–E) covering fresh-install migrations, work-pattern calculations, overtime banking, leave, weekends/holidays, notifications, backup, settings sync, i18n, CSV import/export, salary, crash log, updater semver, Discord guards, goals, and accessibility keys. **Current status:** `PASS: 53 FAIL: 0 WARN: 0`. ### 5.3 GUI smoke tests ```bash python _i18n_gui_test.py # Korean/English label switching python _gui_smoke_test.py # Widget instantiation ``` Both use `QT_QPA_PLATFORM=offscreen` so no real windows appear. - `_i18n_gui_test.py`: 5 cases, **all passing**. - `_gui_smoke_test.py`: 8 cases, **all passing**. ### 5.4 Pre-release test command summary ```bash python -m pytest tests/ -q python _integration_test.py python _i18n_gui_test.py python _gui_smoke_test.py ``` `release.ps1` runs the first two by default (skippable with `-SkipTests`). --- ## 6. Code Style and Conventions ### Identifiers - Classes: `PascalCase` - Functions / variables: `snake_case` - Module-level constants (especially setting keys): `UPPER_CASE` ### Settings keys All setting keys are defined as constants in `core/settings_keys.py`. Import and use the constants; **never use raw strings** for new logic. When adding a new key, also add a default value in `Database.init_default_settings()`. ### Imports Order is typically: standard library → third-party → project modules. Several newer modules use `from __future__ import annotations`. ### Comments and docstrings Code identifiers are English, but inline comments and docstrings are predominantly Korean. New code should follow the existing bilingual style: English identifiers, Korean explanatory comments. ### UI construction - Build widgets in `init_ui()`. - Use helper methods such as `create_*_group()` for readability. - Set `objectName` for QSS styling. - Always verify `self.setLayout(main_layout)` is at the **end** of `init_ui()`, not accidentally indented into a method body. ### Type hints Use type hints on public DB and helper methods (`-> int`, `-> Optional[Dict]`, `-> List[Dict]`, etc.). ### String formatting Use f-strings for internal messages. For user-visible text, use the i18n API: ```python from core.i18n import tr tr('key', name=value) ``` ### No enforced linter There is no `pyproject.toml`, `.flake8`, or `setup.cfg`. Formatting is informal; keep line lengths reasonable and match the surrounding style. --- ## 7. Critical Invariants (MUST PRESERVE) ### 7.1 Time-off subtraction order in `MainWindow.update_display()` Pass the actual `break_minutes` to `TimeCalculator.calculate_remaining_time()`, then subtract `total_time_off = overtime_used + leave_used` from the resulting `timedelta` **after** the call. **Never** mutate `break_minutes` to `break_minutes - overtime_used` before calling. Doing so previously caused a "+29h remaining time" bug. ```python break_minutes = self.db.get_total_break_minutes_today() overtime_used_today = self.db.get_today_overtime_usage() leave_used_today = self.db.get_today_leave_minutes() total_time_off = overtime_used_today + leave_used_today remaining = self.time_calc.calculate_remaining_time(..., break_minutes=break_minutes) remaining -= timedelta(minutes=total_time_off) ``` ### 7.2 Hot-path caching `update_display()` runs at **1 Hz**. DB reads inside it must be cached or throttled: - `cached_time_format` in `MainWindow` - `AutoLunchManager` caches enabled/non-working state - `NotificationOrchestrator` gates periodic checks to 5-minute buckets - Use `_set_text_if_changed()` to avoid useless repaints ### 7.3 24-hour internal time All calculation uses 24-hour `datetime`. 12-hour conversion (Korean "오전"/"오후") happens only in `MainWindow.format_time()`. ### 7.4 Workday boundary `workday_boundary_hour` defaults to 6. Overnight work stays on the previous day's record until that hour. Do not naively use `date.today()` in time logic. ### 7.5 Database invariants - `work_records.date` is `UNIQUE`. - `overtime_bank.work_record_id` and `overtime_usage.work_record_id` are **NULLable** for manual entries. Never filter `WHERE work_record_id IS NOT NULL`. Render NULL rows as "수동 추가" / "Manual". - `leave_records.days` is `REAL` (1.0 / 0.5 / 0.25 / hourly). - Canonical time unit is **minutes** (`work_minutes`). `work_hours` is a derived/floor-synced property. Use `int(minutes) // 60` for hours, not `round()`. - Overtime balance = `SUM(bank.earned_minutes) - SUM(usage.used_minutes)` plus any `INITIAL_OVERTIME_MINUTES`. - WAL mode + 5-second busy timeout is enabled for cloud-sync friendliness (OneDrive/Dropbox). ### 7.6 Migration idempotency Every `migrate_*()` method must early-return if already applied. Use sentinel settings or `IF NOT EXISTS`. Examples: `balance_adjustment_migrated_v2`, `annual_leave_keys_migrated`. ### 7.7 Settings auto-sync pairs `Database.save_settings()` automatically keeps these pairs in sync: - `WORK_MINUTES ↔ WORK_HOURS` (floor division) - `ANNUAL_LEAVE_DAYS ↔ ANNUAL_LEAVE_TOTAL` ### 7.8 Single-file deployment and updater handoff - `main.exe` embeds `updater.exe` via `main.spec` data files. - `_ensure_updater_extracted()` in `main.py` extracts the embedded updater on first launch from `sys._MEIPASS`. - Protect the staging copy at `build/staging/updater.exe`. - `updater.py` is **standalone** (no Qt/network deps). It accepts `--pid`, `--new`, and `--target`, waits for the PID, swaps files with `.bak` rollback, and relaunches. --- ## 8. Security Considerations ### 8.1 HTTP API `utils/http_api.py` is **not present** in the current tree. If it is reintroduced, it must bind to `127.0.0.1` only and remain read-only. Never expose it externally. ### 8.2 Discord webhook - URL is validated against official Discord webhook domains (`discord.com`, `discordapp.com`, canary/ptb). - A browser User-Agent is mandatory; Cloudflare blocks the default Python UA. - Push failures are silent so they do not break the app. ### 8.3 Auto-update - Update check polls a self-hosted Gitea API: `https://kindnick-git.duckdns.org/api/v1/repos/kindnick/Clock_out_Time_Calculator/releases/latest`. - Downloads `main_new.exe` next to the running executable; `updater.exe` performs the swap with `.bak` rollback. - Update apply only works in frozen builds; development `.py` runs are notified but not modified. ### 8.4 DB path handling and cloud sync - `main.py` and `MainWindow` both bootstrap with the default DB first, read `DB_PATH_OVERRIDE`, then reopen with the override path. Do not break this order. - WAL mode + busy timeout tolerates OneDrive/Dropbox sync. ### 8.5 Crash reporting - Gitea issue reporting is **opt-in** via `GITEA_FEEDBACK_ENABLED` + `GITEA_FEEDBACK_TOKEN`. - Tokens are stored as plain strings in the SQLite `settings` table. The UI uses `QLineEdit.Password` echo mode. ### 8.6 Single instance Multiple copies are prevented via `QLocalServer` named `ClockOutCalculatorInstance`. ### 8.7 Debug logging `utils/debug_log.py` only emits output when `CLOCKOUT_DEBUG=1` (or `true`/`yes`). --- ## 9. External Integrations - **Auto-update** (`utils/updater_client.py`): polls the Gitea Releases API. User-Agent: `ClockOutCalculator-Updater/1.0`. - **Discord webhook** (`utils/discord_webhook.py`): optional one-direction push. Browser User-Agent required. - **Gitea Issues** (`utils/crash_handler.py`): optional crash reporting; opt-in only. - **Cloud sync via `DB_PATH_OVERRIDE`**: settings stores the DB path; the app reopens the override path after reading it. - **`holidays` package**: `Database.add_korean_holidays_auto()` returns `-1` if the package is missing, and the UI falls back to `add_korean_holidays()` (8 fixed dates). - **Korean holiday API** (`utils/holiday_api.py`): 공공데이터포털 특일정보 API 키는 `CLOCKOUT_HOLIDAY_API_KEY` 환경변수에서 읽음. 소스코드/바이너리에 키를 하드코딩하지 않음. --- ## 10. i18n Conventions - API: `tr(key, **kwargs)` and `tr_html(key)` from `core/i18n.py`. - Translations live in `_DICT['ko']` and `_DICT['en']`. **Both languages must receive any new key.** - Sentence interpolation uses Python `str.format(**kwargs)`. - HelpView HTML content is in `_HELP_HTML`. - Runtime retranslation is supported via `ui/i18n_runtime.py` using a weakref registry. Register widgets with `register(widget, key, setter='setText', kwargs={}, post=None)` and call `set_language_and_retranslate(lang)` to update them. - UI files (`ui/`) and achievement metadata (`core/achievements.py`) are fully key-based. - Remaining P4 internal-data hardcoding (not user-facing labels) includes DB-stored `leave_type` values and Korean holiday names in `core/database.py`. - A language change may still prompt a restart for widgets not registered for runtime retranslate. --- ## 11. Release Flow `release.ps1 vX.Y.Z` performs the full release locally: 1. **Pre-checks**: verify `$env:GITEA_TOKEN`, no running `main.exe`, no existing tag, no uncommitted changes (unless `-DryRun`). 2. **Bump version**: rewrite `core/version.py`. 3. **Tests**: run `pytest tests/` and `python _integration_test.py` (skippable with `-SkipTests`). 4. **Build**: `updater.spec` → `build/staging/` → `main.spec`. 5. **Code signing** (optional): sign both EXEs if `$env:CODE_SIGN_CERT` is set. 6. **ZIP packaging**: `dist/ClockOutCalculator-vX.Y.Z.zip` containing `main.exe` and `updater.exe`. 7. **Git commit + tag + push**: commit `core/version.py` and `CHANGELOG.md`. 8. **Gitea Release POST**: read `CHANGELOG.md` with UTF-8 to avoid PowerShell 5.1 ANSI mangling, extract the matching section. 9. **Asset upload**: `main.exe`, `updater.exe`, and the ZIP. Use `-DryRun` to preview without git push or API calls. --- ## 12. Past Incidents (Do Not Re-introduce) | Incident | Root cause / fix | |----------|------------------| | +29h remaining time | Mutating `break_minutes -= overtime_used` before calculation. Fixed by subtracting `total_time_off` after `calculate_remaining_time`. | | Manual overtime invisible | Filtering `work_record_id IS NOT NULL`. Now show all rows and label NULL as "수동 추가" / "Manual". | | `annual_leave_total` vs `annual_leave_days` | Two keys for the same value; auto-synced in `save_settings()`. | | Banker's rounding | `round(450/60) = 8` because Python rounds half to even. Use `int(minutes) // 60`. | | FK enforcement rollback | `PRAGMA foreign_keys=ON` conflicted with existing manual overtime records. Kept WAL + timeout, no FK enforcement. | | Discord 403 / Cloudflare 1010 | Default Python UA blocked. Fixed with browser UA. | | Help dialog blank | `self.setLayout(main_layout)` indented into `_reopen_onboarding`. Verify `setLayout()` is at the end of `init_ui()`. | | `dist/updater.exe` wiped by `--clean` | Solved by staging copy at `build/staging/updater.exe`. | | Onboarding auto-skipped | Existing users with `work_records` were auto-completed. Added "Re-run Onboarding" button to Help. | | PowerShell 5.1 ANSI | `Get-Content`/`Set-Content` default to ANSI and mangle Korean in `CHANGELOG.md` and `core/version.py`. Use `[System.IO.File]::ReadAllText`/`WriteAllText(path, UTF8)`. | | PowerShell NativeCommandError | Use `Invoke-Native` helper with `$ErrorActionPreference = 'Continue'` and explicit `$LASTEXITCODE`. | | Frozen chart numpy failure | Added `numpy.core._multiarray_tests` to `main.spec` hiddenimports. | --- ## 13. Additional References - `README.md` — end-user feature overview (Korean/English mixed). - `CLAUDE.md` — additional Korean-language guidance for this project. - `INSTALL.md` — installation and troubleshooting instructions. - `CHANGELOG.md` — release notes in Korean. - `resources/resource_links.md` — links to external resources. When modifying code, update this `AGENTS.md` if you change any conventions, build steps, module map, or external integrations described here.