# Changelog

All notable changes to the Fon2Fix POS system. The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions track the dashboard release.

---

## [18.2] — 2026-05-25 — Pager bar layout fix

### Fixed
- `dashboard.php` — Pagination controls were spread to opposite ends of the card by `margin-left:auto` on the "Per page" label, leaving a large empty gap that looked like the dropdown had escaped the container. Re-ordered to a tight left-aligned group: `◀ Prev` → `Next ▶` → `Per page: [15]` → `Showing 1–15 of 500 • Page 1/34`. All four elements now share the same `gap:8px` from the flex container.

### Operator actions
1. Re-upload only `dashboard.php` + `CHANGELOG.md`.
2. Hard refresh (Ctrl+Shift+R). Badge in the heading should read `v18.2`.

---

## [18.1] — 2026-05-25 — Universal pagination + version badge

**Theme:** UI scalability — keep tabs snappy as job/sale counts climb.

### Added
- **Universal client-side paginator** (`f2fPager`) wired into the three big tabs:
  - 🔧 Drop-off Repairs (`#jobTableBody`)
  - ✅ Billing (`#billingTableBody`)
  - 🛒 Daily Repairs / Sales (`#retailTableBody`)
- Each table now has a control bar above it: **◀ Prev | Next ▶ | Per page: 15 / 30 / 50 / 100 / All | "Showing X–Y of N • Page p/q"**.
- Default page size: **15 rows** per tab. User can switch to 50 or 100 via the "Per page" dropdown; preference is per-tab and resets on page reload.
- Search box (already present on each tab) still works exactly as before — typing filters first, then the paginator slices the filtered set, then resets to page 1.
- **Version badge** (`v18.1`) inline in the dashboard `<h1>` heading. Lets the operator confirm at a glance which build is loaded — particularly useful after a deploy when browser/PHP caches may still serve an older copy.

### Changed
- `filterJobs()`, `filterBilling()`, `filterRetailHistory()` — instead of toggling `row.style.display`, they now set `row.dataset.filterHidden` and call `f2fApplyPagination()`. The visual result is identical; the change lets the paginator distinguish "hidden by search" from "hidden by page slice".
- No data-fetch path changed. API still returns up to 500 rows; the paginator just renders 15 at a time client-side.

### Files changed
| File | Diff |
|---|---|
| `dashboard.php` | +~120 lines (paginator helper, control bars on 3 tables, version badge, filter rewires) |
| `CHANGELOG.md` | This entry |

### Operator actions
1. Re-upload only `dashboard.php` + `CHANGELOG.md`. No DB change, no API change.
2. After upload, do a hard refresh (Ctrl+Shift+R / Cmd+Shift+R). The page title should now show `FON2FIX DASHBOARD <small>v18.1</small>`. If it still shows v18.0 or no badge, the browser cache or PHP opcache still has the old file — see Verification below.

### Verification
- Open Drop-off Repairs tab → you should see exactly 15 rows + the new control bar. Click `Next ▶` → next 15 rows. Change "Per page" to 50 → shows 50.
- Type into the existing search box → filters first, pager resets to page 1.
- Version badge in the `<h1>` should match this changelog version.

### Rollback
- Restore the prior `dashboard.php`. No DB or API rollback needed.

---

## [18.0] — 2026-05-25 — Archive Console (master-only)

**Theme:** Long-term scalability. Keep live tables fast as data grows exponentially by moving old rows into archive tables, with full visibility and reversibility through a new dashboard tab.

### Why
The live `jobs` table holds drop-off repairs, retail sales, and billing all in one place. The dashboard's `fetch_jobs` and `fetch_retail_history` endpoints `LIMIT 500` results — meaning once total retail sales exceed ~500 per branch, older records become invisible in the Billing tab without operator awareness. Archive Console solves this by moving rows older than a configurable age into parallel `*_archive` tables, where they remain searchable but no longer slow down the daily-use queries.

### Added
- **Dashboard tab:** `📦 ARCHIVE` (master-only) between `🗑️ TRASH` and `👥 STAFF`. Five sub-tabs:
  - Drop-off Repairs (`JS-S%` job_ids)
  - Daily Repairs / Sales (`IV-S%`, `model='Retail Sale'`)
  - Billing (same retail data with payment-status lens)
  - Expenses
  - Activity Logs
- **`api.php` actions** (all master-only via `archive_master_gate()`, all CSRF-protected for writes):
  - `archive_preview` — GET, returns count + 5-row sample of rows that would be archived
  - `archive_list` — GET, paginated read of `*_archive` tables with search and date filters
  - `archive_move` — POST, transactional INSERT-then-DELETE; rejects mismatched row counts
  - `archive_restore_one` — POST, single-row restore from archive back to live
  - `archive_restore_range` — POST, bulk restore by date range
- **Helper functions in `api.php`:**
  - `archive_scope_meta(string)` — whitelist of (live, archive, WHERE, ts_col) for each scope. SQL injection vector closed by never accepting table names from the client.
  - `archive_ensure_schema(PDO, live, archive)` — idempotent `CREATE TABLE LIKE` + column-drift self-heal. Adds columns to archive when the live schema evolves.
  - `archive_master_gate()` — single point of role enforcement.
  - `archive_filter_clause(scope, filters)` — bound-parameter builder for status / branch filters (whitelisted values only).
- **Database (created lazily on first move):**
  - `jobs_archive`, `expenses_archive` (clones of their live tables)
  - `activity_logs_archive`, `stock_activity_logs_archive` already created by `archive_logs.php` from v17.2.
- **Activity log entries:** every archive move and restore logs to `activity_logs` with action `ARCHIVE` or `ARCHIVE_RESTORE`.

### Safety mechanisms
- **Preview first** — count and sample shown before any move. Operator sees exactly what will be touched.
- **Typed confirmation token** for batches > 500 rows (must type `ARCHIVE 1247` exactly).
- **Transactional** — INSERT and DELETE happen in one transaction; mismatched row counts roll back automatically.
- **Restore from any archive row** — per-row button + bulk range endpoint. Nothing is permanently destroyed unless the operator manually drops the archive table.
- **Master-only gate** — same `isAdmin()` check used by Staff, Optimize Database, Security Audit.

### Performance profile (designed for 10M+ rows)
- All archive queries are indexed on `date` / `created_at` / `job_id` / `phone`.
- Server-side pagination, hard-capped at 100 rows per page.
- Schema clone via `LIKE` brings indexes with it from the live table.
- Above ~100k rows, the operator can partition `jobs_archive` by `YEAR(date)` (see OPERATIONS.md "Quarterly maintenance"); queries on date ranges then hit a single partition and stay under 100ms.

### Default retention (conservative tier, configurable per operation)
| Scope | Default `days` in dialog |
|---|---|
| Drop-off Repairs | 730 (2 years) |
| Retail Sales / Billing | 365 (1 year) |
| Expenses | 1825 (5 years) |
| Activity Logs | 365 (1 year) |

### Files changed
| File | Diff |
|---|---|
| `api.php` | +~480 lines (helpers + 5 actions + state-changing-actions list) |
| `dashboard.php` | +~270 lines (tab button, panel markup, dialog, JS module) |
| `SYSTEM.md` | New section 3.4.1 documenting scopes + actions |
| `CHANGELOG.md` | This entry |

### Operator actions
1. Upload `api.php`, `dashboard.php`, `SYSTEM.md`, `CHANGELOG.md`.
2. Log in as `endie` or any user with `role='master'`. Click `📦 ARCHIVE`. Empty initially; first move populates the archive tables.
3. Recommended first archive run (dry-run preview, then move):
   - Drop-off: 730 days (move pre-2024 closed repairs)
   - Retail: 365 days (move >1-year-old completed sales)
   - Expenses: 1825 days (only after 5+ years of expense data exists)

### Rollback
- No DB rollback required. Every archive operation is reversible via Restore.
- To remove the feature entirely: restore prior `api.php` and `dashboard.php`. The `*_archive` tables can stay; nothing reads them after rollback. No `DROP TABLE` needed.
- v17.x copies of `api.php` / `dashboard.php` for this release: keep the v17.2 versions you uploaded as your rollback snapshot.

---

## [17.2] — 2026-05-25 — Optimization & Streamlining Pass

**Theme:** Shrink payloads, deduplicate work, prepare schema for growth. Zero behavioural changes; everything in this release is a perf or maintainability win and can be rolled back file-by-file.

### Added
- `archive_logs.php` — operator-triggered (POST + CSRF) move of rows older than N days (default 365) from `activity_logs` / `stock_activity_logs` into matching `*_archive` tables. Transactional INSERT-then-DELETE; idempotent; supports `--dry-run` in CLI and `?cron=1&token=…` in cron mode. Keeps the live tables small as data grows without touching the dashboard's existing log view.
- `OPERATIONS.md` — new "Quarterly maintenance" section with explicit `OPTIMIZE TABLE` / `ANALYZE TABLE` procedure plus when to run `archive_logs.php`.
- `config.features.verbose_log` flag (env `FONFIX_VERBOSE_LOG`) — opt-in switch to restore the routine "Stock OK" `error_log()` lines for debugging. Defaults to off.

### Changed
- `logo.png` — re-encoded from 1024×454 @ 24-bit RGB to 440×195 indexed PNG-8. **209,922 → 7,697 bytes (96% smaller).** Visual quality verified equivalent (no alpha channel in original). Original kept at `backups/pre_patch/20260525/logo.png` for rollback.
- `api.php` — migrations-done marker promoted from `$_SESSION['_migrations_done']` to file (`backups/.migrations_done.v17`). One run per deploy instead of one per session; runtime self-heal block below it still recovers from any miss. The session check is preserved as a first-line short-circuit.
- `api.php` — routine "Stock $action OK" `error_log()` lines (the chattiest in the system) are now gated behind `f2f_verbose_log()`. Failures, mismatches, and "no-match" lines remain unconditional. `stock_movements` table already records every change.
- `log_rotate.php` — extended to prune `backups/ratelimit/` files older than 1 hour. Per-IP rate-limit storage no longer accumulates.
- `dashboard.php` — added `<link rel="preconnect">` and `<link rel="dns-prefetch">` for `cdnjs.cloudflare.com` immediately before the SRI-pinned `<script>` tags. Warms the TLS handshake; saves ~50-150 ms on cold loads.
- `index.php` — removed duplicate `header()` and `ini_set('session.*')` calls. `db.php` is now the single source of truth for security headers and session config; bot detection still runs first in `index.php` (before `db.php` is loaded, so a denied request never hits the DB).
- `backup.php` — replaced `addslashes()` with `PDO::quote()` in the SQL dump value loop. Charset-aware (utf8mb4); correctly handles multi-byte characters that `addslashes()` can corrupt.

### Performance numbers (observed locally)
| Change | Win |
| ----- | ----- |
| logo.png (every page that has the favicon / header) | 202 KB saved per cold load |
| preconnect to cdnjs | ~50-150 ms saved on first dashboard load |
| Migrations marker → file | 14 SHOW/SHOW-COLUMNS queries skipped per session after first hit |
| Verbose-log gating | ~2 disk writes saved per stock-changing API call |
| Header dedup | 8 fewer `header()` calls on the login page |

### Operator actions
1. Upload the files in the patch list below to the live server.
2. Visit the dashboard once and confirm the new logo renders cleanly (rollback path documented above).
3. No DB change required. `archive_logs.php` ships dormant; run it once per quarter when you choose.
4. Optional: after upload, delete `backups/.migrations_done.v17` once so the schema check re-runs. (It re-creates the file itself.)

### Rollback
- Before uploading v17.2 files, **download your current live v17.1 copies** to a local folder (e.g. `live-snapshot-v17.1/`). That is your v17.2-only rollback set.
- `backups/pre_patch/20260525/` holds the **pre-v17.0** originals (before any security hardening). Restoring from there reverts v17.0 + 17.1 + 17.2 together; only use this for a full bail-out.
- No DB rollback is required for any v17.x release.

---

## [17.1] — 2026-05-25 — Post-deploy fixes for first-run findings

### Fixed
- `health_check.php` — removed `retail_sales` from the required-tables list. Retail sales are stored in the `jobs` table (rows where `model = 'Retail Sale'` or `remarks = 'Retail Order'`), not a separate table. Added `stock_movements` to the required list (it is real and used). Added a derived `retail_sales.rows_in_jobs` counter so the dashboard still surfaces POS volume.
- `security_audit.php` — `stale_user` query now also considers `activity_logs.created_at`, with case-insensitive username matching, so accounts that log in via the api.php path or whose username case differs between tables are no longer flagged as `last_ok=never`.
- `security_audit.php` (fix-on-fix) — initial v17.1 query used `COALESCE(ts, '')` which MySQL strict mode rejects as a TIMESTAMP value (SQLSTATE HY000, error 1525). Replaced with `IS NULL` checks in the `WHERE` clause; NULLs preserved end-to-end and rendered as `never` in the finding text.
- `README.md` — removed the documented but never-implemented `CREATE TABLE retail_sales` snippet; replaced with a note explaining the real storage in `jobs`.

### Notes
- These changes don't alter business logic or data. They correct a documentation/health-check drift uncovered the moment the new checkup tooling ran for the first time.
- Operator action: upload the two patched files (`health_check.php`, `security_audit.php`) and the updated `README.md`. No DB change required.

---

## [17.0] — 2026-05-25 — Security Hardening Pass

**Theme:** Patch credential leaks, harden runtime, add regular-checkup tooling. Zero changes to business logic, DB columns, or function signatures.

### Added
- New checkup utilities:
  - `health_check.php` — DB/disk/index/backup-age snapshot. HTML for admins; `?format=json` for cron.
  - `security_audit.php` — read-only audit: failed-login spikes (≥10/24h per IP), stale users (>180d), bulk-delete/refund actions, permissive file perms. Telegram alert in cron mode.
  - `log_rotate.php` — rotates `debug_log.txt` and `backups/sync_log.txt` when >1 MB; prunes archives older than 90 days.
  - `cron_runner.php` — chains uptime → health → log_rotate → security_audit behind a single token-gated endpoint.
- New documentation:
  - `SYSTEM.md` — comprehensive system architecture + file inventory.
  - `SECURITY.md` — current security posture + per-patch inventory (P-001 … P-016).
  - `CHANGELOG.md` — this file.
  - `OPERATIONS.md` — runbook: preflight, secret rotation, cron install, verification, rollback.
- New config infrastructure:
  - `config.sample.php` — committed template (no secrets); copy to `config.php` and customise.
  - `telegram.bot_token`, `telegram.chat_id`, `telegram.enabled` keys in `config.php`.
  - `backup_sync.monitor_token` key in `config.php` (uptime_monitor auth).
  - `security.session_lifetime` key — defaults to current 100-year value; lower to opt-in to shorter sessions.
- `.htaccess` at site root: deny rules for sensitive files/dirs, bot UA block, gzip, browser cache, security headers, commented-out force-HTTPS rule.
- Directories used by new tooling: `backups/ratelimit/`, `backups/archive/`. Auto-created on first run.

### Changed
- `db.php` — env > config > fail-closed credential resolution; HSTS on HTTPS (short max-age 300); `Permissions-Policy`; `Content-Security-Policy-Report-Only` with explicit CDN allow-list; `SameSite=Lax` standardised; `use_strict_mode` enabled; session lifetime now configurable.
- `index.php` — same headers as `db.php`; SameSite changed from `Strict` to `Lax` to fix WhatsApp deep-link breakage.
- `api.php` — same headers; rate limit migrated from per-session counter to per-IP file store under `backups/ratelimit/`.
- `backup.php` — `display_errors=0`; full script wrapped in `try/Throwable`; user-facing failures now show sanitized message, full trace goes to `error_log` only.
- `uptime_monitor.php` — `CURLOPT_SSL_VERIFYPEER=true`; requires `?access_token=` matching `config.backup_sync.monitor_token` (constant-time `hash_equals`); Telegram bot token + chat_id + target URL now read from `config.php`.
- `optimize_database.php` — index creation now requires POST with valid CSRF token. GET fallback accepted only if `&csrf=` companion parameter matches session token.
- `sync_to_backup.php` — removed hardcoded password fallback and hardcoded `secret_key`. Aborts with clear message if credentials missing.
- `helpers.php` — `getConfig()` now reads `FONFIX_*` env vars before falling through to `config.php`, with alias map (`FONFIX_DB_PASS`, `FONFIX_BACKUP_SECRET`, etc.).
- `php.ini` — `exec` added to `disable_functions`; `expose_php=Off`; `session.use_strict_mode=1`, `cookie_secure=1`, `cookie_samesite=Lax`, `sid_length=48`, `sid_bits_per_character=6`.
- `dashboard.php` — pinned floating CDN scripts to specific versions (Chart.js 4.5.0, html5-qrcode 2.3.8) and added SRI `integrity` + `crossorigin` attributes to all 5 CDN script tags. Hashes sourced from cdnjs SRI API.
- `README.md` — minimum PHP requirement bumped 7.4 → 8.1 to match actual cPanel runtime; security and checkup-schedule sections refreshed.

### Removed
- Hardcoded DB password fallback in `db.php` (was line 51-56) and `sync_to_backup.php` (was line 100-105).
- Hardcoded `secret_key` fallback in `sync_to_backup.php` (was line 86).
- `ini_set('display_errors', 1)` block at the top of `backup.php`.
- `CURLOPT_SSL_VERIFYPEER=false` in `uptime_monitor.php`.

### Security
See [SECURITY.md](SECURITY.md) for the full P-001 … P-016 patch inventory and the post-patch threat model.

### Operator action required (post-deploy)
1. `chmod 600 config.php`.
2. Rotate DB password, `backup_sync.secret_key`, Telegram `bot_token` per [OPERATIONS.md](OPERATIONS.md) §2.
3. Generate new `backup_sync.cron_key` and `backup_sync.monitor_token`; update every cron entry that hits these scripts.
4. Install the cron block from [OPERATIONS.md](OPERATIONS.md) §3.
5. Run the verification checklist in [OPERATIONS.md](OPERATIONS.md) §4.

### Rollback
Pre-patch snapshot of every touched file lives in `backups/pre_patch/20260525/`. To revert a single file: copy it back from that folder. To revert `.htaccess`: delete the file (no prior version existed). To revert crons: reinstall the snapshot of `crontab.txt`. See [OPERATIONS.md](OPERATIONS.md) §5.

### Notes / non-goals
- No business logic changed. No DB schema changed (no new tables, no column add/drop on existing tables — the only new on-disk state is files under `backups/`).
- CSP remains report-only because of 585 inline `onclick=`/`style=` attributes in `dashboard.php`. Enforcing it is a future migration.
- Force-HTTPS in `.htaccess` is commented out; enable after the cert audit per OPERATIONS.md §6.
- HSTS `max-age` deliberately short (300s) for rollback safety; bump to 31536000 after a week of stable HTTPS.

---

## [16.2] — pre-2026-05-25 — Baseline

The dashboard, API, and helper modules as they existed before the hardening pass. Captured in `backups/pre_patch/20260525/`.

Key features at baseline (carried forward unchanged into 17.0):

- Repair-job module with auto-generated `JS-{SG|SR}NNNNN` IDs.
- POS with QR scan and refund logic.
- Stock module with smart merge, bulk import, uppercase conversion.
- Reports with Chart.js doughnut/pie/line and Top-Selling table.
- RBAC with granular `can_*` flags; Master / Endie bypass.
- Auto-migrations in `api.php` (idempotent CREATE TABLE / ADD COLUMN blocks).
- Existing per-session API rate limit, login-attempt table, CSRF tokens.

Earlier security notes are captured in [security_improvements.md](security_improvements.md). That document is retained for history and is no longer the canonical security reference — see [SECURITY.md](SECURITY.md) instead.

---

## Versioning policy

- **MAJOR** — breaking change to API, DB schema, or user-visible behaviour. (Last MAJOR: 16.0)
- **MINOR** — new features that are backward-compatible. (e.g. v16.2 added Receipt warranty display.)
- **PATCH** — security/perf changes that touch no business logic. This release (17.0) is technically a security MINOR + tooling MINOR; the version was bumped to 17.0 to clearly mark the deploy boundary.

## How to log future changes

When deploying any change:

1. Add a new dated section at the top of this file: `## [X.Y] — YYYY-MM-DD — Summary`.
2. Group changes under: Added / Changed / Deprecated / Removed / Fixed / Security.
3. Cite the files changed using `[filename.php](filename.php)` markdown links.
4. If the change is security-relevant, also update [SECURITY.md](SECURITY.md) with a new `P-NNN` entry.
5. Snapshot the touched files into `backups/pre_patch/YYYYMMDD/` before uploading (see OPERATIONS.md §1).
