# AssetTrack v11.10 — cPanel Deployment Guide
## School Inventory & Asset Management System (Production MySQL Edition)

> **Release 12.3.18 — hosting on cPanel / Passenger:** see `docs/CPANEL_PASSENGER.md` (step-by-step runbook, keep-warm, memory guard,
> troubleshooting) and `CHANGES-12.3.18.md`. From this release the app **reads `.env` itself** (earlier releases did not, even though
> Step 3 below said so); `.env.example` is included. Values entered in cPanel's *Environment variables* panel always win over `.env`.

---

## What's in v11.10

**Foundation**
- MySQL-backed, with images and documents (avatars, item photos, school logo, procurement bills and quotations, petty cash receipts) stored as files on disk under `uploads/` — not as database blobs. Back up and persist `uploads/` like the database (see *Image & File Storage*)
- FIFO/LIFO stock batch tracking per item, with a permanent purchase log (including the bill image). The original receipt is never rewritten: a return or correction after receiving is added as a separate adjustment line
- Annual budget plans, weekly stock orders and petty cash allocations with approval workflow. A plan's *spent* figure is calculated from its linked records (approved procurement, approved petty cash, quick restocks, repairs), so it cannot drift
- Grid / list / compact inventory views, tag-based search and filtering, Excel reports with filters
- Forgot-password via emailed OTP, themed HTML emails with the school logo
- Production hardening: helmet, compression, rate limiting, graceful shutdown, connection draining, structured error handling

**Added since v11.2** (details in the per-version notes at the end of this file)
- **Permissions:** per-person permission toggles, **user groups**, and a clear precedence (personal → group → role default) — v10/v11, extended with *Edit submitted requests* and *Adjust received purchases*
- **Editing and corrections:** authorised users can edit submitted transfer, procurement, repair and petty cash requests, with a paged **edit log**; consumption and scrap records can be corrected or reverted (v11.3–v11.5)
- **Returns after receiving:** quantity kept and amount paid on a received purchase can be corrected; inventory, stock value and the budget plan follow, in one transaction (v11.9)
- **Asset restocking choice:** adding units to an existing asset asks whether to merge into the lot (weighted cost and date) or create a new entry; the server enforces the choice (v11.6)
- **Excel import:** downloadable template (Items / Instructions / Lists), review-before-import, create or update, in one transaction (v11.4)
- **Money handling:** unit costs keep six decimals, amounts two; half-up rounding applied once, centrally (`money.js`) (v11.8)
- **History kept:** records of deleted items stay as read-only history and are browsable in a **Deleted items** archive (v11.7)
- **Email (SMTP) settings in the app:** administrators set up outgoing mail under **Setup → Organization → Email (SMTP)** (host, port, username, password, From, SSL/TLS) and check it with **Send test email**; the password is stored encrypted. The `SMTP_*` environment variables are now optional (v11.10)
- **Rejection reasons**, quotation-vs-bill handling, report filters, setup renames carried to live records
- **User & Administrator Guide v8.1** (`documents/user-guide.pdf`, shown under *Support*) covering all of the above

---

## Prerequisites

| Requirement | Notes |
|---|---|
| cPanel with Node.js | Version 18+ recommended |
| MySQL 5.7 / 8.0 or MariaDB 10.11+ | Provided by cPanel/WHM |
| SMTP email access | For OTP and notification emails |

---

## Step 1 — Create MySQL Database

1. Log in to cPanel → **MySQL Databases**
2. Create a new database, e.g. `youraccount_assettrack`
3. Create a MySQL user, e.g. `youraccount_atuser` with a strong password
4. Add the user to the database with **All Privileges**
5. Note down: host (`localhost`), database name, username, password

---

## Step 2 — Upload & Extract Files

1. Go to cPanel → **File Manager** → navigate to your domain root (or a subdirectory)
2. Upload the release zip (for example `assettrack_v11_10_0.zip`)
3. Right-click → **Extract**
4. All project files should be in e.g. `/home/youraccount/assettrack/`

---

## Step 3 — Configure Environment Variables

1. Copy `.env.example` to `.env`:
   ```bash
   cp .env.example .env
   ```
2. Edit `.env` with your actual values:
   ```
   DB_HOST=localhost
   DB_PORT=3306
   DB_USER=youraccount_atuser
   DB_PASS=your_strong_password
   DB_NAME=youraccount_assettrack
   SESSION_SECRET=a-very-long-random-string-change-this
   ```
   Outgoing email is **no longer configured in this file**. After the first sign-in, set it up under **Setup → Organization → Email (SMTP)** and press **Send test email** (see *Email (SMTP) settings* below).

> **Optional, for existing installations only:** `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS` and `SMTP_FROM` still work as a fallback if they are present. They are used only until the Email (SMTP) settings are saved in the application; after that, the saved settings win.

> **Session secret:** generate one with:
> ```bash
> node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
> ```

If using cPanel's **Setup Node.js App** panel instead of a `.env` file, add each variable individually under **Environment Variables** — both approaches are supported since the app reads from `process.env`.

---

## Step 4 — Set Up Node.js App in cPanel

1. Go to cPanel → **Setup Node.js App**
2. Click **Create Application**
3. Fill in:
   - **Node.js version:** 18.x or 20.x
   - **Application mode:** Production
   - **Application root:** `/home/youraccount/assettrack`
   - **Application URL:** your domain or subdomain
   - **Application startup file:** `server.js`
4. Add the environment variables from Step 3
5. Click **Create**

---

## Step 5 — Install Dependencies

In the cPanel Node.js app panel, click **Run NPM Install**, or via SSH:
```bash
cd /home/youraccount/assettrack
npm install
```

---

## Step 6 — Seed Initial Data (first run only)

Two seed scripts are available — choose the one that matches how you want
to start:

**Option A — full demo dataset** (for evaluation, training, or a demo
environment):
```bash
node seed.js
```
This creates a realistic starter dataset for "Silver Oak School":
- 5 departments, 8 staff accounts, 10 locations, 8 categories, 16 items
- Stock batches (FIFO and LIFO examples), purchase logs, 4 stocking plans, 4 petty cash expenses
- **Admin login:** `rajesh.shrestha@silveroak.edu.np` / `Admin@123`
- **Staff login:** `anita.rai@silveroak.edu.np` / `Welcome@123`

**Option B — blank install for real use** (recommended for a genuine
school deployment where all data will be entered by hand):
```bash
node seed-blank.js
```
This creates only:
- The school's branding, set to "Gyan Kunj Secondary School"
- One administrator account and one staff account — nothing else
  (no departments, locations, categories, vendors, items, transfers,
  procurement requests, repairs, stocking plans, or petty cash entries)
- **Admin login:** `admin@gyankunj.edu.np` / `Admin@123`
- **Staff login:** `staff@gyankunj.edu.np` / `Welcome@123`

After signing in as the administrator, use **Departments**, **Locations**,
**Categories**, **Vendors**, and **Inventory → Add Item** to build up the
school's real data from scratch. To use a different school name, either
edit the `schoolName` in `seed-blank.js` before running it, or just change
it afterward from **Branding** once signed in.

> ⚠️ Change all default passwords immediately after first login via **My Profile → Change Password**.

> ⚠️ **Never run a seed script against a database that holds real data.** Both scripts save with *prune* enabled, which **deletes** existing rows that are not part of their starter data — `seed.js` replaces departments, users, locations, categories, vendors, items, condition logs, transfers, procurement requests and repair requests; `seed-blank.js` replaces the users table. Run one only once, on a fresh database. To start over, drop and recreate the database first.
>
> Upgrading an existing installation does **not** involve a seed script — see *Upgrading from an Earlier Version* below.

---

## Step 7 — Start the App

Click **Start App** in the cPanel Node.js panel, or via SSH:
```bash
node server.js
```

On startup the app will:
1. Connect to MySQL
2. Auto-create all tables and safely `ALTER TABLE` any missing columns on existing installs (safe to run repeatedly)
3. Start listening on the configured port
4. Begin periodic housekeeping (expired OTP cleanup every 30 minutes)

The app also handles `SIGTERM`/`SIGINT` gracefully — in-flight requests (e.g. a large bill upload) are allowed to finish before the process exits, which matters on cPanel redeploys/restarts.

---

## Step 8 — Configure Your Domain

In cPanel → **Domains** or **Subdomains**, point your domain/subdomain to the Node.js app port. cPanel's Passenger/reverse proxy handles this automatically.

---

## Email (SMTP) settings

Outgoing mail (password codes, welcome emails, request notifications, decision notices) is configured in the application, by an administrator, under **Setup → Organization → Email (SMTP)**:

| Field | Notes |
|---|---|
| Allow sending email | Master switch. When off, nothing is sent (emails are logged to the server console instead). |
| SMTP host / Port | For example `smtp.gmail.com` and `587`. |
| Username / Password | The password is stored **encrypted** (AES-256-GCM) and is never sent back to the browser. Leave the box blank when saving to keep the saved password. Use an app password for Gmail or Microsoft 365. |
| From email / From name | Optional. Blank sends from the username, named "&lt;organization&gt; &lt;app name&gt;". |
| Use SSL/TLS from the start | On for port 465; off for port 587 (STARTTLS). It follows the port as you type. |

**Send test email** saves the details first, then sends a test message to the signed-in administrator's own address, and reports a plain-language reason if it fails (wrong password, host not found, port/SSL mismatch). Changes apply immediately — no restart.

- **Upgrading:** if `SMTP_*` environment variables are present, email keeps working exactly as before until the settings are saved on this screen; saving then switches to the saved details and carries the existing password over unless a new one is typed.
- **Encryption key:** the password is encrypted with `SETTINGS_ENCRYPTION_KEY` if you set it, otherwise with `SESSION_SECRET`. If that value is ever changed, the saved password can no longer be read and the screen asks for it to be entered again. Use a long random value and keep it stable.
- Requires the *Manage organization & branding* permission (administrators by default).

---

## Upgrading from an Earlier Version

1. **Back up first:** export the database and copy `uploads/`.
2. Replace the application files with the new release, keeping your `.env` and the `uploads/` directory. Run `npm install` and restart the Node.js app.
3. **Do not run `seed.js` or `seed-blank.js`** — they are for a fresh database only (see Step 6).
4. On first start the application upgrades itself. Watch the log for these lines; none needs action:
   - **Schema:** new tables and columns are added where missing (for example `deleted_items`, `procurement_requests.actual_cost` / `received_quantity`, `purchase_logs.kind`), plus a few indexes.
   - **Unit-cost precision:** the unit-cost columns (`items.purchase_cost`, `stock_batches.unit_cost`, `purchase_logs.unit_cost`, `consumption_logs.unit_cost`, `scrap_items.original_unit_cost`) are widened to `DECIMAL(18,6)`. This is lossless — existing values are kept exactly.
   - **Budget plans:** each plan's *spent* total is rebuilt from the records linked to it. If an older version's running counter had drifted, plan figures can change once, to match their lists.
   - **Email settings:** a new `email_settings` table is created. If you configured mail with `SMTP_*` variables, nothing changes until an administrator saves the Email (SMTP) screen.
   - **Stock batches:** stock items that had a quantity but no FIFO/LIFO batches (typed in, imported, or created before batches existed) receive an opening-balance batch at their own cost, so later issues are costed correctly.
5. **Optional — rebuild exact unit costs** for goods received before v11.8, from the recorded amount paid: `node repair-unit-costs.js` shows a preview (changes nothing); `node repair-unit-costs.js --apply` makes the changes. Read the preview and back up first. Items typed in or imported by hand cannot be repaired this way.
6. **Permissions:** role defaults reproduce earlier behaviour, so nothing changes for anyone until an administrator switches a permission or assigns a group. New permissions appear with sensible defaults (for example *Adjust received purchases* is administrator-only).
7. **Browsers:** the service worker cache version changes with each release; users may need one hard refresh (Ctrl+F5) to pick up new screens and styles.
8. **Dashboard figures** may differ slightly from before: *Pending approvals* now counts only requests still awaiting a decision, and *Low stock* no longer counts disposed items.
9. **User guide:** `documents/user-guide.pdf` is replaced by the new edition; the Support screen reads its version, release date and page count from `SUPPORT_DOCS` in `server.js`.

---

## Post-Deployment Checklist

- [ ] Log in as admin and change the default password
- [ ] Update school name, tagline and **logo** under **Branding** — the logo appears in the sidebar, the login page, and every outgoing email
- [ ] Add your departments, locations and categories
- [ ] Invite staff via **Staff & Users** → Add User (they receive a welcome email with a temporary password, if SMTP is configured)
- [ ] Set up annual/weekly/petty stocking plans under **Stocking Plans**
- [ ] Review **Setup → User groups** and each person's permissions (administrators have everything; managers and staff follow role defaults until changed)
- [ ] To load existing inventory in bulk, use **Inventory → Export Import Template**, fill it in, then **Import Records** and review before confirming
- [ ] Make sure `uploads/` is included in your backups
- [ ] Set the petty cash per-expense limit if different from the Rs. 5,000 default (Settings)
- [ ] Set up outgoing email under **Setup → Organization → Email (SMTP)** and press **Send test email**
- [ ] Test the forgot-password OTP flow end-to-end
- [ ] Verify email notifications are working (check SMTP settings and server logs — if SMTP isn't configured, emails are logged to the console instead of failing silently)

---

## Image & File Storage

Images and documents (avatars, item photos, school logo, procurement bills, purchase-log bills, petty cash receipts) are stored on **disk**, under an `uploads/` directory next to `server.js` — not in the database. This is deliberately chosen for cPanel and shared hosting generally: serving a file from disk is far cheaper than round-tripping it through Node and a MySQL query every time, it keeps the database small and fast to back up, and it avoids an entire class of bug where a large BLOB column bloats every unrelated query unless carefully excluded.

```
uploads/
├── avatars/                    (public — served via /api/images/avatar/:id)
├── items/                      (public — served via /api/images/item/:id)
├── logo/                       (public — served via /api/images/logo)
└── private/
    ├── bills/                  (auth required — procurement bills)
    ├── purchase-logs/          (auth required — permanent audit-trail copies)
    └── petty-receipts/         (auth required — petty cash receipts)
```

The `/api/images/...` URLs stay the same as earlier versions — the app resolves the stored file path (a small database lookup) and streams the file with `res.sendFile()`, which natively supports range requests and conditional GETs. "Public" here only means "no database round-trip and no sensitive content" (an avatar or item photo isn't private); bills and receipts stay behind the same `requireAuth` checks as before.

### ⚠️ This directory must persist across deployments

On cPanel, if you redeploy by re-extracting a fresh zip over your application root, **make sure `uploads/` is excluded from that overwrite** (or back it up and restore it after), the same way you'd protect a database — this directory now holds real, irreplaceable data (uploaded photos, bills, receipts) that doesn't exist anywhere else. The app creates the folder structure automatically on startup if it's missing, but an empty `uploads/` folder means every previously-uploaded image is gone.

**Recommended:** include `uploads/` in your regular backup routine alongside your MySQL database dump — the two together are the complete state of the application's data.

Purchase log entries (the permanent audit trail created whenever procurement is received into inventory) keep their **own copy** of the bill file on disk, independent of the original procurement request's bill — so if someone later removes or replaces the bill on the procurement request itself, the purchase record's copy is untouched.

Upload limits: all uploads (item/avatar/logo photos and bills/receipts/PDFs) are capped at 256 KB — deliberately tight, since shared-hosting proxies/WAFs (the cPanel + ModSecurity combination this was tuned against) commonly impose their own low, often-undocumented multipart body-size ceiling that a larger "valid" upload could otherwise sail past client-side and still get silently killed by. Photos are automatically compressed in the browser before upload to fit under this; PDFs cannot be auto-compressed, so a large PDF bill/receipt needs to be shrunk manually before uploading. The upload widgets validate file size and type in the browser before sending, so oversized or wrong-type files are rejected immediately with a clear message rather than after a slow upload attempt.

---

## File Structure

```
assettrack/
├── server.js            # Main Express application (routes, approvals, requests, reports)
├── db.js                # MySQL data layer — pool, schema and migrations, FIFO/LIFO batches,
│                        # purchase logs, stocking plans, petty cash, deleted-items register
├── money.js             # The one place money rounding is defined (amounts 2 dp, unit costs 6 dp)
├── permissions.js       # Permission registry, groups and role defaults
├── itemImport.js        # Excel import: template builder, validation, review plan
├── fileStorage.js       # Filesystem storage for images/documents (uploads/)
├── mailer.js            # Themed HTML email system (OTP, notifications, welcome)
├── exports.js           # Excel report builders (inventory, transfers, etc.)
├── nepaliDate.js        # Bikram Sambat date utilities
├── seed.js              # Full demo dataset seeder (fresh database only — see Step 6)
├── seed-blank.js        # Minimal seeder — admin + staff only (fresh database only)
├── repair-unit-costs.js # Optional one-time unit-cost repair (preview by default)
├── package.json
├── .env                 # Environment variables (NOT committed to git)
├── .env.example         # Template
├── documents/           # PDFs shown under Support (user guide, licence agreement)
├── uploads/             # Uploaded files — see "Image & File Storage" above;
│                        # back this up like a database, don't wipe on redeploy
└── public/
    ├── index.html       # Login + forgot password (OTP flow)
    ├── dashboard.html   # Main application shell + all modals
    ├── sw.js            # Service worker (installable app shell cache)
    ├── icons/           # App and PWA icons
    ├── css/style.css    # Full design system
    └── js/
        ├── common.js          # API wrapper, toast, themed confirm dialog, money rounding, upload validation
        ├── app.js             # Full application logic
        ├── itemImport.js      # Import review screen
        └── nepaliDatePicker.js# AD/BS date picker
```

---

## Troubleshooting

| Problem | Solution |
|---|---|
| `ER_ACCESS_DENIED_ERROR` | Check `DB_USER` and `DB_PASS` in `.env` |
| `ECONNREFUSED` on port 3306 | Verify `DB_HOST=localhost` and MySQL is running |
| OTP email not sending | Check SMTP settings; look at Node.js error logs — unconfigured SMTP logs the OTP to the console instead of failing |
| `Cannot find module 'mysql2'` (or similar) | Run `npm install` again |
| Session lost on restart | Expected to *not* happen — sessions are stored in MySQL via `express-mysql-session`, not in memory |
| Duplicate-key error running `seed.js` | The database already has seed data; drop and recreate the database first if you need a clean reset |
| Uploaded file rejected as "too large" | All uploads are capped at 256 KB. Photos are auto-compressed by the browser before upload; a PDF bill/receipt over the limit needs to be shrunk manually first |
| A photo/bill/avatar disappeared after an unrelated edit | This was a known issue in early builds where saving any row in a table could wipe blob columns for other rows; v3's data layer fixes this by using `INSERT ... ON DUPLICATE KEY UPDATE` scoped to non-blob columns only, verified against a live database before release |
| Uploads return `EACCES`/permission denied, or `ENOENT` writing to `uploads/` | The Node process needs write permission to the `uploads/` directory. On cPanel this is usually automatic (the app runs as your account), but if you manually created the folder via File Manager first, check its permissions (755 for directories) and ownership match your cPanel user |
| Uploaded images/bills disappeared after a redeploy | The `uploads/` directory wasn't preserved across the redeploy — see "Image & File Storage" above. Back it up like a database; don't let a fresh zip extraction overwrite it |
| Images uploaded fine locally but "nothing happens" on cPanel | See the dedicated **Diagnosing "Uploads Do Nothing" on cPanel** section below — as of this version, every upload route logs the real cause to the Node.js error log, so check that first |

---

## Diagnosing "Uploads Do Nothing" on cPanel

If an upload (photo, avatar, logo, bill, receipt) appears to silently do
nothing on cPanel while working locally, work through this in order —
each step rules out one specific, well-known cause.

### 1. It is very unlikely to be a relative-path problem

Every file path in this app is built from `__dirname` (the actual
directory the `.js` file lives in on disk), never from `process.cwd()`
(the directory the process happened to be *launched from*). This
distinction matters specifically because Passenger/cPanel can spawn the
Node process from a different working directory than the app's own
folder — code that built paths like `./uploads` or `path.resolve('uploads')`
would break in exactly that scenario, but `__dirname`-based paths (what
this app uses throughout — see `fileStorage.js`) resolve correctly
regardless of where the process was started from. `require('./db')`-style
module imports are similarly always resolved relative to the requiring
file, never the working directory, so those are not a factor either. In
short: this specific class of bug has already been designed around and
verified in the code, so it's an unlikely root cause — treat the steps
below as the more probable culprits first.

### 2. Check the Node.js error log

Every upload route now logs the real underlying error before responding
— go to cPanel → **Setup Node.js App** → your app → and open the error
log (or tail it via SSH: `tail -f /home/youraccount/logs/yourapp/error.log`
— exact path shown in the cPanel panel). Trigger the failing upload again
and watch for a line starting with `[upload]`. This will show you the
*actual* exception — permission denied, disk quota exceeded, an unexpected
field name, etc. — instead of a blank client-side failure.

If the log shows `multer rejected ... Request aborted`, this specific
message means the client's connection closed before the file finished
uploading — it is not a permissions or disk problem. Confirm you're
running a version of `public/js/common.js` where file uploads use a
longer client-side timeout than regular API calls (120s vs 30s) — on a
real network + reverse-proxy hop (cPanel/Passenger), even a modest image
can take longer than a plain JSON request to fully transfer, and earlier
versions of this app applied the same short timeout to every request
including uploads, aborting the upload from the browser side before it
completed. This never showed up on localhost because uploads there
complete near-instantly over loopback.

### 3. Check for an intermediate proxy/WAF rejecting the request

Shared hosting frequently runs ModSecurity or a similar Apache-level
firewall in front of Node apps, and its default rule sets sometimes flag
multipart/form-data POSTs (especially larger ones) before they ever reach
your Node process — in that case nothing will appear in *your* app's
error log at all, because the request never got there. Check cPanel's
main **Apache error log** (not the Node.js app's own log) for entries
around the time of the failed upload, and ask your host to whitelist the
app's upload endpoints if ModSecurity is confirmed as the cause.

### 4. Confirm `uploads/` exists and is writable

The app creates the full `uploads/` folder tree automatically on boot.
Check cPanel's File Manager to confirm it exists next to `server.js`, and
that its permissions allow the app's own user to write to it (755 for
directories is standard on cPanel; ownership should match your cPanel
account automatically since the app runs as that user).

### 5. Check disk quota

If the cPanel account is at or near its disk quota, writes will fail with
`ENOSPC` — this now surfaces clearly in the Node.js error log per step 2
above, but is easy to overlook as a cause if you're not expecting it.
Check **cPanel → Disk Usage**.

### 6. Confirm the Node.js version matches `package.json`'s `engines` field

This app requires Node 18 or newer (declared in `package.json`). In
**cPanel → Setup Node.js App**, confirm the selected version is 18.x or
later — an older selected version can cause `npm install` to silently
install incompatible package versions or fail in ways that only surface
once a specific code path (like a file upload) is exercised.

---

---

## Progressive Web App (Installable)

AssetTrack can be installed as an app on desktop and mobile — an "Install
app" button appears in the sidebar (once signed in) and on the login page
once the browser determines the app is installable. Once installed, it
opens in its own window/icon without browser chrome, same as a native app.

- The install icon uses the school's uploaded logo automatically (falls
  back to a generic AssetTrack icon if no logo has been uploaded yet) —
  see `GET /manifest.json` in `server.js`, which is generated per-request
  rather than a static file for exactly this reason.
- **Requires HTTPS.** Browsers only offer installability over a secure
  context (or `localhost` in development) — this is a browser platform
  requirement, not something this app can override. If the install button
  never appears in production, confirm the site is actually being served
  over `https://`, not `http://`.
- iOS Safari has no programmatic install prompt at all (Apple platform
  limitation) — tapping the install button there shows instructions for
  the manual Share → "Add to Home Screen" flow instead.
- `public/sw.js` is a minimal service worker that only caches the static
  app shell (HTML/CSS/JS/icons) for faster repeat loads. It deliberately
  never caches anything under `/api/` — inventory data, sessions, and
  approvals are per-user and constantly changing, so caching them would
  serve stale or incorrect data rather than just being a minor
  inconvenience.

---

## Security Notes

- Change `SESSION_SECRET` to a unique random value per installation
- Use HTTPS (cPanel's Let's Encrypt SSL) in production
- The `.env` file must never be publicly accessible — prefer cPanel's Node.js app environment variables panel for secrets
- The mail server password entered in the application is stored encrypted and never returned to the browser (see *Email (SMTP) settings*)
- OTP codes expire after 10 minutes and are single-use; sessions last up to 12 hours
- New users and administrator password resets start with a temporary default password — make sure people change it at first sign-in
- Rate limiting: login (30/15 min), OTP requests (5/10 min)
- `helmet` sets standard security headers; `compression` reduces response payload sizes
- The app exits gracefully on `SIGTERM`/`SIGINT` and logs (without crashing) on unhandled promise rejections

---

## Multiuser Production Readiness — Audit Summary

This section documents what was checked when auditing the app for memory
leaks and correctness under real multiuser production load, so future
changes can be checked against the same list.

**Sessions — MySQL-backed, not in-memory.** Login sessions are stored via
`express-mysql-session`, not `express-session`'s default in-process
`MemoryStore`. This matters two ways: the default MemoryStore is
explicitly documented as unsuitable for production (it never evicts
expired sessions on its own, so its memory footprint grows without bound
for as long as the process runs), and it is also process-local — under
Passenger, which can spawn more than one Node worker process for the same
app, sessions stored in one process's memory are invisible to another,
so a person could get logged out at random depending which worker
happened to handle their next request. The MySQL-backed store avoids both
problems, and the app also runs its own periodic cleanup
(`db.cleanOtps()`, every 30 minutes) to keep the sessions/OTP tables from
growing unbounded over time.

**No unbounded in-memory state.** Audited every module-level `let`/`const`
for anything that accumulates over the app's lifetime — no growing
arrays, Maps, or caches were found anywhere in `server.js`, `db.js`, or
`mailer.js`. The only long-lived timers are the OTP cleanup interval and
the graceful-shutdown force-exit timer, neither of which retains
per-request data.

**Connection pooling — sized for shared hosting, not leaked.** The MySQL
pool (`db.js`) and the session store's own pool are both bounded
(`DB_CONNECTION_LIMIT`, default 8; `SESSION_CONNECTION_LIMIT`, default 3)
and every connection acquired via `withTx()` is released in a `finally`
block even on error, so a failed request can't hold a connection open.
If you increase cPanel's Node.js app to run more than one instance, the
total connections used is roughly `(DB_CONNECTION_LIMIT +
SESSION_CONNECTION_LIMIT) × instance count` — keep that comfortably under
your MySQL user's connection quota (check with your host if unsure).

**Uploads never buffer to disk before processing.** `multer.memoryStorage()`
keeps an uploaded file entirely in a request-scoped RAM buffer with no
temp-file step; that buffer is only referenced for the lifetime of the
request handler and is never assigned to any module-level variable, so
it's eligible for garbage collection the moment the request completes —
verified by inspecting every upload route in `server.js`.

**Rate limiting is per-process.** `express-rate-limit`'s default store is
also process-local, same caveat as the old session behavior above. This
is fine for the standard "single Node.js app instance" cPanel setup this
app is designed for; if you deliberately run multiple instances behind a
load balancer, be aware rate limits are tracked separately per instance
(each instance allows the full configured limit independently) rather
than shared — this is a security/correctness nuance to know about, not a
memory leak.

**Batched writes, not per-row.** The data layer's `save()` function
upserts changed rows in a small number of multi-row batches rather than
one round-trip per row (see the comment above `save()` in `db.js` for
the history of why this matters) — under concurrent multiuser load this
keeps individual requests fast and avoids connection-pool exhaustion from
many slow sequential queries piling up.

**Email sends never block a response.** Every notification email is
fired through `notifyAsync()` (a fire-and-forget wrapper with its own
error logging) rather than being awaited inline — a slow or failing SMTP
server affects only the background email, never the person waiting on
their approve/reject/submit action to complete.


---

## v8 notes — identifiers, saving and depreciation policy

- **Item codes (INV-000123)** come from a row-locked counter that only moves forward, so deleting, merging or disposing of an item never frees its code for reuse. On every start-up the counter is raised past the highest code already stored (safe after a database restore or import), duplicate codes left by older installs are reassigned (the oldest row keeps its code; a warning is logged), and the unique index is guaranteed. Codes are minted only after a request has passed validation.
- **Record ids** (itm_, loc_, usr_ …) now use a cryptographically random suffix instead of `Math.random()`.
- **Saving no longer deletes by omission.** `db.save()` writes only rows that changed since they were loaded and never removes rows that are merely absent from the array; deletes are explicit (`deleteOne` / `deleteMany`). This closes a race where a slightly stale request could delete newly created records or bring deleted ones back. Seed scripts pass `{ prune: true }` to keep their replace-everything behaviour.
- Password-reset OTPs use a secure random generator.
- Depreciation: the applied policy is shown on each asset (asset > category > organization), the method dropdown lists Category default and Organization default separately, and automatic modes lock the rate / useful life / salvage fields to the resolved values.
- The service-worker cache name was bumped so browsers pick up the new front-end.


---

## v9 notes — production-scale data access

- **No whole-table loads for large tables.** Items, transfers, procurement requests, repairs and consumption logs are never read in full any more. Endpoints fetch one row by id (`db.getById`) or run filtered, sorted, paged SQL (`db.queryItems`, `listTransfers`, `listProcurement`, `listRepairs` …). Location scoping, search, sorting and totals all run in the database.
- **Paged inventory.** `GET /api/items` returns one page (`page`, `pageSize`, default 25, max 1000) plus `total`, `totalPages`, and `summary`/`groups` for the whole filtered set. `GET /api/items/lookup` (type-ahead pickers, max 200 rows) and `GET /api/items/counts` (per-location / per-category counts) are new.
- **Dashboard aggregates in SQL**, cached per visibility scope for up to 60 s and cleared on any write in the process. Asset book value is computed by a chunked scan over a narrow column projection, so memory stays flat.
- **Reference-data cache.** users, locations, categories, departments, vendors and app settings are cached in memory for `REF_CACHE_TTL_MS` (default 5000; `0` disables). Writes through `db.js` invalidate immediately; with several worker processes another worker can serve data up to one TTL old.
- **Exports** are filtered in SQL and refuse to run above `EXPORT_MAX_ROWS` (default 50000) with a clear message telling the user to narrow the filter.
- **Indexes** for the filtered/sorted queries are created automatically at start-up if missing (`ensureIndex` in `db.init`).
- **Search** uses `LIKE '%term%'` over name, tag, serial, model, manufacturer and code. That is fine into the tens of thousands of rows; beyond that add a FULLTEXT index and switch `itemWhere()` to `MATCH ... AGAINST`.
- Front-end: item pickers are search-as-you-type against the server; the service-worker cache name was bumped so browsers pick up the new scripts.


---

## v10 notes — per-user permissions

- **Where:** Setup ▸ Users ▸ Add/Edit user ▸ *Permissions*. 39 switches in 8 groups (inventory, transfers, procurement, repairs, scraps, budgets & petty cash, reports & financial visibility, setup). The Users table shows *Role defaults* or *N custom* per person.
- **How it resolves:** effective permission = the user's override if one is stored, otherwise the role's default. Administrators default to everything, but every switch except *Manage users & permissions* can still be turned off for an individual admin. *Manage users & permissions* is always on for administrators and cannot be given to anyone else.
- **Nothing changes on upgrade.** Role defaults reproduce the behaviour of v9. The two older per-user dropdowns (dashboard access, scrap access) are converted into overrides once at start-up (`users.permissions`, a TEXT column added automatically).
- **Enforced on the server** (`requirePerm(...)` in `server.js`; registry in `permissions.js`), and mirrored in the UI by `data-perm` attributes — controls a user may not use are hidden. Changes apply to the very next request; the user's open page refreshes its menus on reload.
- **Location/department shortcuts:** custodians can still restock their own location and custodians / department heads can still update repairs for their locations — unless an administrator switches *Quick-restock* / *Update repair requests* **off** for that person, which blocks the shortcut too.
- **View depreciation & book values** off: depreciation figures are removed from every API response (lists, item detail, picker, dashboard tile), the depreciation report and Configure controls disappear, and the item form can no longer change depreciation settings.
- **Safety nets:** the last active administrator cannot be demoted, deactivated or removed; users cannot remove themselves.
- Editing the permission list: add a row to `PERMISSIONS` in `permissions.js`, then guard the route with `requirePerm('your_key')` and (optionally) add `data-perm="your_key"` to the button.


---

## v11 notes — user groups and a clearer permissions screen

- **User groups** (Setup ▸ User groups): a named set of permission settings maintained in one place. Each permission in a group is **Role default**, **On** or **Off**; anything left on *Role default* keeps following each member's role. A user belongs to at most one group (set on Setup ▸ Users ▸ Edit).
- **Precedence, highest first:** the user's own override → their group's setting → their role's default. A personal override can therefore still grant or remove a single ability for one member. Admin-only / locked permissions (*Manage users & permissions*) can never be set by a group.
- **Group changes apply immediately** to every member (next request). When a group is saved, members' personal overrides that merely repeat the group's setting are dropped so the *Custom* count stays meaningful. Deleting a group never deletes people — members fall back to their role defaults plus any personal overrides.
- **Explicit "Off" from a group** blocks the location shortcuts too (custodian restock, custodian/department-head repair updates), exactly like a personal "Off".
- **Permission switches** now show a green **On** / red **Off** pill and a colored switch, with *Custom* (personal change) and *Group: name* (inherited from the group) tags; the overlapping layout was caused by the generic form-field styles leaking into the switches and is fixed.
- **Database:** new `user_groups` table and `users.group_id` column, created automatically at start-up. No existing data is changed.


---

## v11.1 notes — request visibility, plan detail, labels

- **Who can see which requests** (procurement log, transfers, repairs, and the Recent activity feed on the dashboard). Location access still decides which *items* a person can see; a new rule (`requestVis()` in `server.js`) decides whose *requests* they see: final approvers for that kind of request (`approve_transfers`, `approve_procurement` / `receive_procurement`, `manage_repairs`) see all of them; everyone else sees requests raised by themselves, their manager, people who report to them, or people in their own department(s), plus requests touching a location they look after (custodian, or the head of the owning department). Requests with no location, or involving a "shared access" location, no longer show up for unrelated people. The same rule applies to the Excel exports and to the procurement requests listed inside a budget plan.
- **Not changed:** consumption logs and an item's own history still follow location access; petty expenses are controlled by the *View petty expenses* permission.
- **Budget plan detail** now lists everything charged against the plan in one timeline — petty cash expenses, procurement requests and quick restocks — whatever the plan type. Previously a petty plan only showed petty expenses, so quick restocks linked to it (which do count towards its spent total) were invisible.
- **Setup ▸ Organization:** the "Login page copy" card is now **Login page customization**.


---

## v11.2 notes — required costs

- **Procurement requests** (new-item and restock requests, the cart, single and batch submit, and requests made from a budget plan) now require an **Estimated cost**. **Quick restock** requires the **Total amount**. **0 is accepted** (free / donated items) but must be typed in — a blank field is always rejected and never defaulted to 0; negative or non-numeric values are rejected. Both are enforced on the server (HTTP 400 with `field: 'estimatedCost' | 'totalCost'`) as well as in the forms.
- Forms show a themed inline warning under the field (`showFieldError()` in `public/js/common.js`: red border + tint, icon, message, focus + scroll, clears as you type). The same helper now handles the required checks on the procurement item name / justification / restock item and the petty-expense description and amount.
- Existing records without a cost are untouched; repair estimates remain optional (the cost isn't known until a repair is assessed).
- To use the helpers elsewhere: `const n = requireAmount('field-id', { required: '…', negative: '…' }); if (n === null) return;` (zero allowed, blank rejected) or `requirePositiveNumber(...)` (must be above zero).

- **Typed-only amounts (v11.2.2):** money fields (`class="amount-input"` — procurement est. cost, quick-restock total, petty expense amount, item unit cost, repair est./actual cost, plan budgets, petty cash limit, asset salvage value) are plain text boxes that accept only a typed amount: digits and one decimal point, at most 2 decimals. No spinner arrows, no mouse-wheel or Up/Down-key counting, letters and signs are blocked, and pasted text such as `Rs. 1,250.50` is reduced to `1250.50` (a pasted minus sign is dropped; zero stays valid). All other number boxes (quantities, rates, years) lose focus when the wheel turns over them, so scrolling the page never changes a value. Add the class to any new money input; the handlers are delegated in `public/js/common.js` so dynamically created modals work too.

---

## Support section — user guide & licence agreement

The **Support** item in the sidebar previews two PDFs inside the app (read-only, not downloaded) and shows the developer's contact details.

PDFs live in the `documents/` folder next to `server.js` (outside `public/`, so they are only served to signed-in users):

| File | Shown as |
|---|---|
| `documents/user-guide.pdf` | Support → User guide (included) |
| `documents/licence-agreement.pdf` | Support → Licence agreement (add when ready) |

- Drop in or replace a file with that exact name; no restart is needed. Size and "last updated" are read from the file.
- Version / release date / page count shown above the viewer are set in `SUPPORT_DOCS` in `server.js`.
- Contact details are set in `SUPPORT_CONTACT` in `server.js`; override email, phone and website per deployment with `SUPPORT_EMAIL`, `SUPPORT_PHONE`, `SUPPORT_WEBSITE`.
- Include `documents/` in your backups and deploy uploads.


## v11.3.0 — rejection reasons, request corrections, report filters

- **Rejection reasons.** Rejecting a transfer, procurement request or petty cash expense — as a manager, an administrator, or from the dashboard quick-review — now opens a themed dialog that requires a reason (the server enforces it too). The reason is stored (`manager_decision_note`, `admin_decision_note`, petty `decision_note`), shown next to the Rejected badge in every table (eye icon), included in the notification emails, and exported in a **Rejection Reason** column. New columns are added automatically on startup.
- **Editing submitted requests.** New permission **Edit submitted requests** (`edit_requests`, group "Editing submitted requests"; admin on by default) — it appears automatically in the per-user toggles and in User groups. Holders get an **Edit** button on transfers, procurement, repairs, petty cash and the manager's Team approvals. Every save writes one row to the new `request_edit_logs` table (who, when, optional note, field-by-field before → after). Edited requests carry an **Edited ×N** pill that opens the log; the Excel logs gain an **Edits** column. Fields lock once they would disagree with reality: a decided transfer's destination/quantity, a received procurement's item/quantity/cost, and a received petty purchase's amount/item. Cost corrections on approved requests move the linked stocking plan's spent total by the difference. Repair *status* still uses Update.
- **Report filters.** Reports has a Filters card: department, location (narrowed by department), category, vendor, condition, item type, depreciation method, status, priority, request type and — for administrators — the person. Only filters relevant to the selected report are shown. Preview and export share one builder (`buildReport`), so they always match, and every exported sheet prints a **FILTERS APPLIED** block under the title (or "None — all records are included").
- Exports of transfers / procurement / repairs are no longer clamped at 5,000 rows by the list query; the existing `EXPORT_MAX_ROWS` guard still applies.

## v11.3.1 — report filter fixes, budget links on edits & repairs, quotation vs bill

- **Report filters returned nothing for some reports.** A department filter used to match only the department stored on the item/record, which is often empty (items just sit in a department's location). It now matches a department's own records *plus* anything in its locations — transfers: either end of the move, the item's department or the requester's department; repairs: its locations or items; procurement: the requester's department, a restocked item or the department's budget plan; consumption: used for it or at its locations. The location filter on procurement now also matches the requester's base location (not only restocks).
- **Budget plan on edits.** Editing a procurement request, repair or petty expense now has a *Budget plan* picker (add, change or remove the link — also on already-approved requests). The plan's *spent* total moves with it; the edit log records the old and new plan.
- **Repairs can be charged to a budget plan** (annual, weekly or petty): *Budget plan* in the repair Update dialog. A repair counts its actual cost, or the estimate until the actual cost is known; cancelled repairs count for nothing; deleting a repair gives the amount back. Linked repairs appear in the plan's activity list. New columns: `repair_requests.stocking_plan_id`, `plan_counted_plan_id`, `plan_counted_amount`.
- **Quotation vs bill.** A procurement request now holds a **Quotation / invoice** (attach it when creating the request, or later from the *Quotation* column) until the goods are received — new column pair `quote_path` / `quote_filename`, stored in `uploads/private/quotes/`. The **bill** is uploaded in the Receive dialog (or afterwards from the *Bill* column), is rejected for requests that have not been received, is copied onto the permanent purchase history, and is shown on the item page (*Purchase bill*) and in the Purchase history tab with **View bill**. A failed bill upload during receiving is now reported instead of being silently ignored. On first start, anything already attached to a not-yet-received request is moved across as its quotation.


## v11.4.0 — inventory import from Excel

- **Inventory ▸ Export Import Template** downloads an `.xlsx` (Items / Instructions / Lists sheets, with dropdowns fed from your own categories, departments, locations and vendors) that can be handed to a client to fill in.
- **Inventory ▸ Import Records** reads the filled file and shows a *Review before importing* window: every row is marked Create, Update, No change or Error, with the exact field changes, errors and warnings. Nothing is saved until **Confirm & import**; the file is re-validated at that moment and written in a single transaction.
- Rows are matched to existing items by Item Code, then Asset Tag, then Serial Number (stock items: name + category + location). Blank cells never erase saved data. Categories, locations, departments and vendors must already exist; they are never created by import.
- Dates may be typed as AD or BS (Miti); see the Instructions sheet. The review window has an AD / BS / Auto-detect switch.
- New permission **Import inventory from Excel** (`import_items`, admin by default). Creating rows also needs *Add inventory items*, changing existing items needs *Edit items & photos*.
- New table `item_import_logs` (created automatically) records who imported what and when. No new npm packages (uses the existing `exceljs` and `multer`).


## v11.5.0 — consumption & scrap corrections, paged edit log

- **Consumption ▸ + Record consumption** records usage straight from the Consumption section: pick the stock item from a searchable list, then quantity, date used, purpose and notes.
- **Edit / Revert** on each consumption record (new permission *Edit & revert consumption records*, `edit_consumption`, admin by default). Changing the quantity corrects the item's stock on hand and its FIFO/LIFO batches; **Revert** puts the whole quantity back and keeps the record, flagged as reverted (hidden from totals, reports and item history; visible with the status filter). The record, the stock and the log entry are saved in one transaction.
- **Scraps ▸ Edit** corrects a scrap record's original unit cost, depreciated value, disposal date and notes. Inline value changes are now logged too (still needs *Revalue or remove scrap records*).
- Consumption and scrap records get the same **Edited ×N** pill and **Edit log** as requests. The log is now paged (8 per page), and long values wrap instead of overflowing.
- Database: new columns on `consumption_logs` (`status`, `reverted_*`) are added automatically on start. Existing records stay active.

## v11.5.1

- Dashboard **Total items tracked** and the by-location breakdown no longer count disposed items (they are in Scraps), so the total now matches the Inventory list.

## v11.5.2

- **Fixed: budget link on petty cash expenses not saving.** Editing a petty expense and choosing (or changing/removing) its budget plan reported success but never wrote the link, so the expense kept reappearing "without budget" and never showed under the plan. The link is now saved.
- **Fixed: wrong "spent" amount on budget plans.** A plan's spent total is now always calculated from the records linked to it (approved petty expenses, approved procurement requests, quick restocks, non-cancelled repairs) instead of a running counter that drifted. This also stops double counting when a decision was repeated, and gives money back when a petty expense is deleted.
- **Existing data self-heals:** on start, every plan's spent total is rebuilt from its linked records. No manual action needed.
- A petty expense that adds stock no longer appears twice on a plan (as the expense and as its auto-created request); it is counted once.
- Petty expenses linked to a plan at creation are now checked (plan must exist and be active).
- Deleting a plan now really detaches its linked records, as the confirmation message says.
- Older weekly plans with only a week start date now show a period (7 days) instead of "No period set".
- Plan detail marks pending/rejected items as "not counted yet".

## v11.6.0

Valuation and stock-integrity fixes from the logic audit.

- **Transfers can no longer be applied twice.** Admin and manager decisions on transfers and procurement requests are final (a repeated "approve" used to move stock again). At approval the app re-checks that the item is still at the requested location and that enough stock remains; otherwise it refuses with a clear message instead of driving stock negative.
- **Transfers keep total value constant.** Stock moved to a record that already exists at the destination is now added at the cost it actually left the source with, blended with the destination's own cost (before, the moved units took the destination's price). The move is one database transaction, and never lands on a disposed record.
- **Restocking an asset lot no longer resets its age.** The purchase date becomes the quantity-weighted average of the old and new units, so existing units keep their depreciation. Stock items are unchanged.
- **Actual cost and quantity at receiving.** *Mark as received* now asks for the quantity received and the total amount paid (pre-filled from the request). Item cost, stock quantity, purchase history, depreciation base and budget spend use these real figures. Requests created from a petty expense keep the petty amount.
- **Stock cost layers now drive value.** After stock is consumed or transferred out, the item's unit cost is re-set to the weighted average of the FIFO/LIFO layers that remain (before, it stayed at the old blended average).
- **No more Rs 0 consumption costs.** Any shortfall in batches is costed at the item's own unit cost. Stock items with a quantity but no batches (typed in, imported, or older records) get an opening-balance batch automatically on start.
- **Consuming stock is atomic.** Quantity check, batch deduction, stock update and re-pricing happen in one locked transaction, so two people using the same stock at once can't over-issue it.
- **Editing a stock item's quantity or unit cost** (Edit item) now adjusts its batches as well, so the ledger keeps adding up to the stock on hand.
- **Validation:** procurement quantity must be greater than zero (single and cart requests), and a budget plan linked on a procurement request must exist and be active.
- **Grouping asset lots** now requires the same name and depreciation settings, and refuses disposed, damaged or under-repair items.
- Cached dashboard totals are refreshed after every database transaction.
- Database: new columns `procurement_requests.actual_cost` and `received_quantity` are added automatically on start.

## v11.6.1

- **Restocking an asset now asks first.** Adding units to an existing depreciating asset blends its cost and purchase date, so the app explains this and lets the person choose **Add to existing lot** or **Create a new entry** (or cancel):
  - when adding a restock of an asset to a procurement cart or a petty cash expense (choosing "new entry" turns it into a normal new-item request), and
  - when the goods are received. Here the dialog shows the exact before/after figures (units, cost per unit, purchase date). The server enforces this: receiving an asset restock without an explicit choice is refused.
- Choosing "new entry" books the units as a separate record that depreciates from its own date and cost. Stock (consumable) restocks are unchanged and don't ask.
- A restock is no longer merged into a *disposed* record at a different location.

## v11.6.2

- **Dashboard "Pending approvals" no longer over-counts.** Transfers and procurement requests that were already rejected (for example by the manager, which leaves the admin decision at "pending") are no longer counted or listed as awaiting approval. The Approve/Reject buttons on the Transfers and Procurement pages are likewise hidden for requests that are no longer pending.
- **Low stock no longer includes disposed items,** matching the inventory list and the other dashboard figures.

## v11.7.0

**Records of items removed from inventory are now kept as read-only history.**

- When an item is deleted (or merged into another lot), a record of what it was and when it left is saved in a new `deleted_items` table (created automatically). Nothing linked to the item is deleted: its purchase, consumption, transfer, repair, condition and scrap records all remain.
- Those records are **greyed out** wherever they appear (Transfers, Procurement, Repairs, Consumption, Scraps, My location, Team approvals and the dashboard lists) with the notice *"Item no longer in inventory"* (or *"merged into another lot"*). Hover for details such as the date and who deleted it.
- They **cannot be edited**: Edit/Update/Delete/Restore buttons are disabled, and the server refuses the same actions (edit requests, repair updates, bill uploads, consumption edits and reverts, scrap revaluation). A pending transfer or request for a removed item can only be declined, not approved or received.
- New **Inventory → Deleted items** archive (for people who can see every department and view purchase history): lists each removed item with totals bought and used, and opens a read-only view of its purchases, usage, transfers, repairs and condition changes.
- Items deleted *before* this version have no saved details; their records are still locked and listed in the archive as "Deleted (before removal tracking)".
- Clicking a removed item from a log now explains it no longer exists instead of showing a generic error.
- Also fixed on the way: item names on the "My location" screen are now escaped, and the delete-item confirmation escapes the name.

## v11.7.1

- **Renames in Setup now carry over.** Matching is by ID.
  - *Vendor:* items, and requests/repairs that are still open. (Previously nothing updated.)
  - *Location:* items (as before), plus the scrap register, pending transfers and open repairs.
  - *Category:* items, procurement requests and petty expenses (as before), plus the scrap register.
  - *Department:* locations (as before), plus budget plans and petty expenses.
  - Completed history (received purchases, consumption and purchase logs, finished transfers, closed repairs) keeps the name as it was at the time.
- Added three database indexes (`procurement_requests.stocking_plan_id`, `repair_requests.stocking_plan_id`, `petty_expenses.linked_procurement_id`) used by the budget plans' spent-total calculation. Created automatically on start.

## v11.8.0

**Money rounding is now handled in one consistent way.**

- **One rule, one place** (`money.js`, mirrored in the browser): amounts of money are kept to 2 decimals, a cost *per unit* to 6 decimals; rounding is half-up on the decimal value (1.005 → 1.01 — JavaScript's `toFixed`/`Math.round` gave 1.00 and 1 and disagreed with the database); and a figure is rounded **once**, at the end of its calculation. A rounded number is never fed into another calculation.
- **Unit costs keep their precision.** `items.purchase_cost`/`salvage_value`, `stock_batches.unit_cost`, `purchase_logs.unit_cost`, `consumption_logs.unit_cost` and `scrap_items.original_unit_cost` are widened to `DECIMAL(18,6)` automatically on start (existing values are kept exactly). 12,000 sheets bought for Rs 1,250 are now valued at Rs 1,250.00, not Rs 1,200.
- **Weighted averages** (restock, receiving, group, transfers, the cost refresh after stock leaves) blend with the exact amount paid and keep 6 decimals, so repeated moves no longer shave off value.
- **Consumption costs** come from the exact cost of the batches used, rounded once. Editing a quantity no longer rebuilds the total from a rounded unit cost (a Rs 100 / 3-unit record edited to 6 units is Rs 200.00, not 199.98).
- **Depreciation is worked out on the whole lot** and rounded once; book value + accumulated depreciation now equals cost to the paisa. Per-unit figures are for display only.
- **Every amount is rounded once when it is saved**, whichever route it came in by (requests, receiving, petty cash, budgets, repairs, scraps, import).
- **Display and exports:** costs per unit show up to 4 decimals; export headline totals show 2 decimals (they used to be whole rupees) and are summed unrounded, then rounded once.
- **Optional:** `node repair-unit-costs.js` previews (and, with `--apply`, repairs) unit costs of goods that were received through procurement before this version, by rebuilding them from the recorded amount paid. Read the preview and take a backup before applying. Items typed in by hand or imported cannot be repaired this way (the original total was never recorded).

## v11.9.0

**Returns and corrections after goods have been received.**

- A received procurement request can now be corrected when something goes wrong (damaged goods sent back to the vendor, wrong quantity delivered, a different amount paid). In **Procurement → Edit** of a received request, a new box **"Correct this purchase after receiving"** lets an authorised person change the **Quantity kept in inventory** and the **Amount actually paid**, with a live preview of the effect. A **reason is required**.
- One transaction keeps everything in step:
  - **Inventory count and stock value.** For stock items the units come off the delivery's own batch (cost layers stay correct); for asset lots the returned units and their cost come out of the lot. Only units from that delivery that are still on hand can go back. If some were already used or moved, the app says how many can be returned and changes nothing.
  - **Budget.** The plan's spent total is recalculated from the new amount paid, so the money is released (or charged) on the linked plan. A request that came from a petty cash expense keeps its petty expense amount in step.
  - **Purchase history.** The original receipt is never rewritten; a separate **Returned / Correction** line (negative quantity and amount) is added to the item's purchase history.
  - **Edit log.** The change, the inventory count before → after, and the reason are saved in the request's edit log with who made it and when.
- New permission **"Adjust received purchases (returns & corrections)"** (administrators by default; also needs "Edit submitted requests"). Lists now show the quantity received and the amount actually paid, with the original estimate underneath when they differ.
- Not changed: the ordered item, category, quantity and the original estimate stay as they were. If the inventory item was deleted or disposed, the purchase is read-only history.
- Also fixed in `repair-unit-costs.js`: it used the wrong column name for the link between stock batches and procurement requests (it would have errored, not damaged data). If you ran an earlier copy it would have failed immediately; this version is correct.

## v11.9.1

**Consistent row buttons.**

- **Cause of the oversized buttons:** a desktop-width style re-sized `.btn` and `.btn-sm` but not `.btn-xs`, so every "extra small" button (Consumption Edit/Revert, Scraps Edit/Restore, Petty cash Edit/Delete) rendered at full default size on desktop. That is fixed, and all row actions now use the same size as Transfers and Repairs.
- **Layout:** action columns now share one rule, so buttons sit in a single evenly spaced row (8px gap) and never stack on top of each other, and the column hugs its content. Procurement's Approve / Decline / Receive / Edit are one row.
- **Visibility:** soft ("ghost") buttons get a faint outline so they no longer blend into a hovered row and look like plain text.
- Small utility buttons (pagers, "Remove", "Clear all", the dashboard quick-approve icons) are now genuinely small, as originally designed.

## v11.9.2

- **User & Administrator Guide v8.0** replaces v7.0 (`documents/user-guide.pdf`; the Support screen now shows version 8.0). New and updated chapters cover editing and the edit log, returns after receiving, asset restocking options, budget linking, the Excel import template, authentication, permission toggles and user groups, deleted-item history, rounding, report filters, and a quick how-to reference.
- **README refreshed** for v11.9: current feature list, an *Upgrading from an Earlier Version* section, an updated file structure, and a corrected warning about the seed scripts (they delete existing rows, so they must only be run on a fresh database).

## v11.10.0

**Email (SMTP) settings are now managed in the application.**

- New **Email (SMTP)** card under **Setup → Organization**: *Allow sending email* switch, SMTP host, port, username, password, optional From email and From name, and *Use SSL/TLS from the start* (follows the port: 465 on, 587 off). **Save email settings** and **Send test email** buttons; the test saves first and sends to the administrator's own address, with plain-language error messages.
- The password is encrypted at rest (AES-256-GCM, key from `SETTINGS_ENCRYPTION_KEY` or `SESSION_SECRET`) and is never returned to the browser; leaving it blank keeps the saved one. Changes apply immediately — the mail connection is rebuilt automatically, no restart.
- With *Allow sending email* off, nothing is sent. Existing installations that use `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` / `SMTP_FROM` keep working unchanged until the screen is saved; the saved settings then take over.
- Needs the existing *Manage organization & branding* permission. New table: `email_settings`. New routes: `GET/PUT /api/admin/email-settings`, `POST /api/admin/email-settings/test`.
- **User & Administrator Guide v8.1** adds Section 13.8 (Email (SMTP) Settings), a what's-new row, a how-to row and an FAQ entry.

## 12.3.15 operations notes

* **`DB_QUEUE_LIMIT`** now defaults to **500** (was 30). Requests that arrive while all `DB_CONNECTION_LIMIT` connections are busy wait in this queue instead of failing; the number of MySQL connections used is unchanged. Only if the queue itself overflows does the server answer `503` (and log `Connection pool saturated`).
* **`op_locks`** is a small table created automatically. It holds a row for the few milliseconds an approve/receive request is being processed (and is removed afterwards); stale rows expire after 120 s. It is what makes these actions safe when several app instances share one database.
* **Login hashing** runs in up to two worker threads (`bcryptPool.js`). Nothing to configure; it needs only Node's built-in `worker_threads`.
* **Rate limiting** of `/api` is per signed-in user (600 requests/minute each); anonymous requests are limited per IP. `trust proxy` must stay correctly configured so anonymous/login limits use the real client IP.
* **QA suite:** `npm run test:qa` (see `tests/qa/README.md`). It writes data — use a test database.
