# Important Change Requests

Tracked, high-impact changes that are known but deliberately deferred so they
can be done carefully (rather than slipped into unrelated work). Newest first.

---

## CR-001 — Incremental dialplan sync (fix slow extension saves)

**Status:** OPEN — not started
**Priority:** HIGH (scales badly; this is the root cause of slow/hanging saves)
**Area:** `app/Http/Controllers/ExtensionController.php` → `syncHotDeskDialplan()` /
`doSyncHotDeskDialplan()`
**Raised:** 2026-07-02

### The problem (this is why saving an extension hangs)

Every time **one** extension is created, edited, or deleted, the sync method
**wipes and regenerates the `from-internal` dialplan rows for EVERY extension on
the system**, not just the one that changed. Concretely, `doSyncHotDeskDialplan()`:

1. `DELETE`s `asterisk_dialplan` rows for all extensions (context `from-internal`,
   priorities 1–50, plus the `from-queue` rows).
2. Rebuilds ~18–30 rows **per extension** for **all** extensions in memory.
3. Bulk-inserts the whole set back.

Cost is **O(number-of-extensions) on every single save**:

| Extensions | Rows rebuilt per save |
|-----------:|----------------------:|
|        100 |                ~1,800 |
|        500 |               ~9,000 |
|      5,000 |              ~90,000 |

At 500 extensions this already produced the MySQL error
`1390 Prepared statement contains too many placeholders` (a single INSERT
exceeding 65,535 placeholders). That specific error is now mitigated by
**chunking the insert** (commit v1.1.168), but chunking only stops the crash —
**it does not fix the underlying slowness**. Every save still rebuilds the whole
site's dialplan, so saves get slower and heavier as an install grows, and a
large client (thousands of extensions) will see the extension save "hang".

### The fix

Make the sync **incremental**: when a single extension changes, regenerate only
**that extension's** dialplan rows (delete + reinsert just its `from-internal`
and `from-queue` rows), instead of rebuilding everyone.

Suggested shape:

- Extract the per-extension row-building into a pure function
  `buildDialplanRowsForExtension($extId, ...context): array` (no DB writes).
- Add `syncOneExtensionDialplan($extId)` that deletes + reinserts **only** that
  extension's rows in a transaction, then does one dialplan reload.
- Keep the current "rebuild everything" path but rename it clearly
  (`resyncAllExtensionsDialplan()`) and call it ONLY from:
  - the "PBX Defaults changed" save (a global default legitimately affects all
    extensions), and
  - an explicit maintenance/repair command.
- Point `store()`, `update()`, `destroy()` at the single-extension path.

### Constraints / risks (be careful here)

- The dialplan is safety-critical. Preserve **byte-identical** output for the
  unaffected extensions. Verify a full-rebuild vs incremental produces the same
  rows for a sample extension before/after.
- Watch the shared pre-computed context (CFNA scope map, recall settings, Teams
  config, sub-device map) — the single-extension path must resolve the same
  inputs the bulk path does.
- Keep the priority layout / offset logic (`$ofs`, `$rtoOfs`) identical.
- Delete range must stay `priorities [1,50]` for the affected extension only.

### Interim mitigation already shipped

- v1.1.168: chunked `asterisk_dialplan` inserts (`array_chunk(..., 5000)`) to
  stay under the MySQL placeholder limit. Prevents the crash on large sites but
  does not remove the per-save full rebuild.

### Related

- `php artisan extensions:prune-unused` was added (v1.1.168) to remove
  extensions with no CDR activity; it does a single reload at the end rather
  than per-extension, and is the right pattern for bulk operations.
