477 lines
22 KiB
Markdown
477 lines
22 KiB
Markdown
# 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.
|