Version 2.8.3 — 2026-08-13
A documentation correction. No application code changed in this release - the only difference from v2.8.2 is the bundled PDF guides. Schema v50, unchanged.
Fixed
- The Administrator Guide no longer recommends SSH authentication the app cannot do.
Q5000 connects to the PBX with a username and password:
ServerModelcarriesSshUsername/SshPassword, and everySshClientinSshServiceuses the password constructor. There is no key-based path anywhere in the product. The shipped guide nevertheless recommended key-based authentication in three places, and section 6.3's hardening list ended with "Disable password authentication, use keys only" - an administrator following it locked the application out of the PBX permanently, with nothing to go on but a generic "Cannot connect to PBX". The troubleshooting section then contradicted the advice by asking the reader to verify password auth was enabled.- Section 4.1 now states the supported method before the reader configures anything,
and warns explicitly against
PasswordAuthentication nofor the Q5000 account. - Section 6.3 drops both key bullets for controls that do apply: a long unique
password on the dedicated SSH account, account restriction (noting
/sbin/nologinis unusable, since the tunnel needs a shell), IP whitelisting promoted as the strongest control available, and credential rotation. - Troubleshooting replaces the
authorized_keyspermission check with the failure administrators actually hit - keys-only hardening rejecting a correct password.
- Section 4.1 now states the supported method before the reader configures anything,
and warns explicitly against
- The same claim is corrected in
docs/CDRReporter.DocGenerator, which is no longer referenced by the solution but would have reintroduced it if revived.
Version 2.8.2 — 2026-08-03
The final-review release: a four-way deep code review of everything since v2.7.9 plus a full end-to-end drive-through on a fresh install, with every finding fixed. Schema v50, unchanged - no migration in this release. (v2.8.1 was never published; this release supersedes v2.8.0 directly.)
Added
- Review Budgets covers parents with their own extensions. A department holding
both its own extension mappings and child departments was invisible to the scan,
while
BudgetAlertServicestill charged its direct spend against an effective budget that is only its children's sum - the scan could report "everything sized" on the exact shape that alerts chronically. Two remedies, by shape:- An unbudgeted parent with its own spend and no budgeted descendant now claims the subtree as the suggestion target: one budget at the parent, sized to the same rolled spend the alert engine measures there. Budgets on its leaves alone could never cover the parent's own calls.
- A parent whose own extensions spend materially (R100+/month) beneath a budgeted descendant gets a "Spend not covered" advisory row: nothing can be applied anywhere for that spend, so the row explains the shape and suggests moving those extensions into a sub-department. Advisory rows cannot be ticked, are skipped by Select All, and sort to the bottom.
- Demo Mode seeds 4 months of history (~900 calls over 90 business days, same ~10-calls-per-business-day density). The old 30-day window forced Review Budgets into "limited history" mode on every demo and left comparative reports with nothing to compare. The deliberate specials (international policy-trippers, long calls) stay pinned to recent days so Call Policies still shows fresh violations.
Fixed
- Per-user report tables agree with their report headers on DID-extension sites.
ChainUserAttribution.Resolve()- which drives the per-user tables in Daily Summary, User Activity and Call Detail - was the one chain consumer the v2.8.0 "configured extension is internal whatever its shape" fix missed: it still classified direction on number shape alone, so on a site whose extensions are 10-digit DIDs the same report called a chain Internal in its header and Outbound in its per-user table. Resolve now takes the same known-extensions set, and a new test pins header/table agreement. - A full rate-file upload takes effect immediately. Upload Rate File imported the CSV but never invalidated the singleton rate cache (only the SA quick-adjust path did), so reports and verification kept pricing against the replaced rates until the next sync, reclassify or restart.
- The quarterly reminder counts calendar months. "Last checked N months ago" used
day-count division (TotalDays / 30.44), which truncated a 4-calendar-month span to
"3" whenever it crossed February - and failed the reminder's unit test every March
- and said "2 months ago" on the very days the 3-month reminder first fires.
- An explicit R0 budget with real usage is always flagged. The alert engine treats R0 as a real budget (any spend reads 100%+ and fires Critical monthly), but the scan's R100 materiality floor called it "Looks right". Always material now.
- A "not enough history" scan no longer counts as a review. It stamped the quarterly reminder timestamp and wiped any explicit snooze, suppressing the reminder for three months over budgets that were never actually checked.
- A mid-apply failure refreshes the Departments screen. If applying suggestions failed partway (e.g. locked database during a sync), the budgets already written stayed invisible behind a stale tree until a manual refresh.
- Demo data internal consistency. Budget right-sizing now clamps the rounded budget into the intended gauge band (nearest-R5 rounding could drop Sales Outbound to 79.9% and lose its warning); the acknowledged Finance history alert keeps its original coherent 75% amounts instead of being restated below every live threshold; and a right-sizing failure logs a warning instead of reporting the already-committed seed as failed.
- Cross-machine video sync can no longer pair a stale hash with newer recordings. end.ps1's unchanged-folder skip republished the server hash sidecar unconditionally; if another machine had pushed in between, the next pull skipped the download and the next push overwrote their recordings. The skip path now only seeds a missing sidecar, never overwrites one.
- Updater failure marker survives single-digit exit codes.
echo %rc%>fileparses a single-digit code as a stream redirect and writes an empty marker; the redirection now comes first. - Test-suite flake root-caused and fixed.
Recent_Calls_Are_Loaded_From_Demo_Dataintermittently found RecentCalls empty: the InitialLoads test seam's unlocked read-modify-write dropped a chained load when await-continuations raced on pool threads under the test dispatcher. TrackLoad is now locked in the four ViewModels that chain incrementally.
Changed
- Informational dialogs look informational.
DialogService.ShowInfowas an alias for the success dialog, so every info message app-wide - including "AI phone lookup requires a monthly subscription" - rendered under a green tick titled "Success". Info messages now show a cyan Information header. - The Budget Review dialog's Suggested column header follows the configured currency symbol instead of a hardcoded "(R)".
- Setup-wizard demo copy matches what the seeder actually creates (900 calls over 4 months, 7 departments, 21 phonebook contacts).
- Test suite grown to 2,173 checks (1,709 Core + 464 Desktop).
Version 2.8.0 — 2026-07-30
Feature release introducing Review Budgets, which sizes department budgets from what departments actually spend, plus the fixes found after v2.7.9. Schema v50, unchanged - no migration in this release.
Added
- Review Budgets - usage-based budget suggestions (Departments screen). A budget
that is never approached can never fire the 80% warning, and one that is breached
every month trains people to ignore alerts; the new scan finds both. It measures each
budget-owning department's spend over the last three complete calendar months and
proposes
max(average x 1.15, busiest month x 1.10)rounded up to the nearest R50. Sizing to the busy month rather than the average is deliberate - a budget set just above the average is breached by any normal busy month. Results open in a review dialog showing the per-month figures, average, busiest month, current budget and a plain-language verdict; every row is editable and individually selectable, and only ticked rows are written.- Suggestions are confined to the one department per branch that actually owns its
budget, following the existing
DepartmentBudgetCalculatorrule. Root and branch departments are excluded because the department dialog forces their budget back to null ("Auto-calculated"), so a value written there would be silently wiped. - Only material gaps are listed - more than 20% of the current budget and more than R100. Well-sized departments are reported as a count, and a department with no calls at all is never given an R0 budget.
- The current, part-elapsed month is excluded; including it would drag every average down. A young database uses however many complete months it has and says so.
- Suggestions are confined to the one department per branch that actually owns its
budget, following the existing
- Quarterly budget review reminder on the Outbound Dashboard's Department Budget
Status card, shown once the last review is over three months old (or has never
happened) and there is a complete month of call history to measure against.
Review Now navigates to Departments and starts the scan; Ignore hides it for
another quarter after explaining the risk. State lives in two new
settingskeys (BudgetReviewLastRun,BudgetReviewSnoozedUntil) - no schema change.
Fixed
- Rate cache is no longer torn out from under a running cost calculation. v2.7.9
made
CostCalculationServicea DI singleton so a post-importClearCache()reached every consumer, but the class had never been written to be shared: its reads re-read_rateCacheafter their null guard, so a rate import running during a sync could see non-null then null and throw (GetRateStatsworst of all - past the guard, thenMax()over a cleared cache). All reads now snapshot once under the cache lock. - A failed migration 50 no longer stamps the database as v50. Each of the four
ADD COLUMNstatements was wrapped in a blanket catch and the version advanced regardless, so one failure left a permanently half-migrated database that never re-ran and madeBillingRepositoryfail forever on "no such column: contact_phone" (VerifyRequiredTablesAsyncchecks tables, not columns, so it couldn't catch it). The catch is narrowed to "duplicate column name"; anything else leaves the version below 50 so the next startup retries. - Internal calls are internal whatever the extension looks like. Moving the
dashboard tiles onto the chain classifier in v2.7.9 dropped the
extension_mappingsexclusion, and the classifier decides on shape alone (3-6 digits). On a site whose extensions are 10-digit DIDs every internal extension-to-extension call read as outbound, while the cost-by-category and daily-trend queries on the same screen still excluded them - one dashboard disagreeing with itself. - A silent update that fails now says so. On any msiexec exit code other than 0/3010 the hidden batch jumped straight to cleanup and deleted itself - no window, no relaunch, nothing logged, because the app had already closed. Declining the UAC prompt or an MSI rollback looked like the application had simply vanished.
- A scheduled contact-scoped billing run is named after the contact. v50 taught
scheduled runs to honour a rate card's individual-contact scope, but the invoice
"BILLED TO" line and the billing-history Filter column still showed the job's whole
category. The rule now lives in
ResolveClientRunDisplayName, matching the interactive path.
Changed
- Dashboard refreshes load call chains once, not three times.
GetPeriodTotalsAsyncandGetBusyHourChartsAsynccover the identical window, so every refresh materialised each leg twice on top of the summary's own month-wide load - millions of objects per refresh and per server switch on a large multi-PBX site. One load now feeds both.
Internal
- Ancestor spend rollup extracted from
BudgetAlertService.GetRolledUpSpendAsyncintoDepartmentSpendCalculator.RollUp, so budget alerts and the budget review can't drift apart on what a parent department spent. - 30 new tests (2,166 total, all green), covering the peak-aware formula and R50 rounding, the materiality filter, both over- and under-provisioned verdicts, the rollup into a budget-holding parent, root exclusion, partial-month exclusion, the limited-history path, and the reminder's visibility and snooze rules.
Version 2.7.9 — 2026-07-30
Consolidation release rolling up the Q5000 rebrand, an individual-contact billing scope, a dashboard-tile accuracy fix, and two internal refactors. Schema v50 (the billing contact-scope migration; upgrades automatically from any prior version).
Changed
- Rebranded to Q5000 across all user-visible strings (installer, window titles, wizard, dashboards).
- Dashboard summary tiles count call chains, not CDR legs. The Today / This
Month / Total tiles now group legs by the canonical chain key and classify
direction with the same
ChainUserAttributionrule (plus the unmapped-src override) the busy-hour charts use, so ring-group / queue fan-out no longer inflates the tiles and internal extension-to-extension calls are excluded from the "inbound + outbound" totals. Tiles and charts now agree by construction (pinned by a regression test); outbound cost is the full chain leg-cost sum.
Added
- Individual-contact scope on billing runs and rate cards (schema v50): a billing run or rate card can now be scoped to a single phonebook contact.
Internal
BackupRestoreServicesplit from a ~2,900-line class into a thin facade plus focusedBackupWriter,RestoreEngineandBackupArchiveIocollaborators underServices/Backup/(public surface and behaviour unchanged; a stale.gitignorerule that was hiding the new source folder was also fixed).- DI cleanup: the five remaining dialogs take
LocalDatabasethrough their constructor instead of theLocalDatabase.Sharedstatic;ReportGeneratorFactoryreceives its services; andCostCalculationService/RateFileServiceare now singletons so a rate re-import's cache clear reaches every consumer. - Test suite expanded to 2,127 (Core 1,670 + Desktop 457), all green.
Version 2.7.8 — 2026-07-27
Dashboard accuracy release, prompted by a live-site anomaly: a handset left off-hook in a queue overnight generated ~19,000 zero-second BUSY retry legs on a single call, painting a flat ~44 calls/day floor across every after-hours bar of the Total Busy Hour chart. No schema change (stays v49).
Fixed
- Busy-hour charts count call chains, not CDR legs. Both dashboard busy-hour charts now group legs by the canonical chain key and count each call once, attributed to its representative leg's hour (the Daily Summary rule), so ring-group/queue fan-out and retry legs can no longer inflate any hour.
- The Total chart matches its "inbound + outbound" label. Direction comes
from the shared report rule (
ChainUserAttribution.ClassifyDirection) plus the Call Policies unmapped-src override (extension-shaped originating src of plausible length), so internal extension-to-extension traffic is excluded and partially-mapped sites still count unmapped extensions' outbound calls. - Chains straddling the period boundary classify correctly. Legs load with a one-day margin around the window and chains count only when their representative leg falls inside it.
- Chart header subtitles wrap instead of clipping at the card edge ("daily average, last 30 days, inbound + outbound" was cut off).
Changed
- Both busy-hour charts are computed in a single pass over the window
(
GetBusyHourChartsAsync), and the scan runs off the UI thread — dashboard refreshes on large date ranges no longer freeze the window. - The charts' known-extension/PIN set now comes from the same repositories +
BuildKnownInternalhelper the reports use, removing a parallel definition.
Version 2.7.7 — 2026-07-20
Maintenance release, functionally identical to 2.7.6. No schema change (stays v49). Cut so machines already on v2.7.6 can exercise the new auto-relaunch update path end to end (the relaunch code runs in the installed version, so updating to 2.7.6 still used the old flow).
Version 2.7.6 — 2026-07-20
Small fix release on top of 2.7.5. No schema change (stays v49).
Fixed
- AI Security Alert extension-anomaly feed populates when a server is selected.
Once the mapping join was added, the per-server filter referenced an ambiguous
server_id(present on bothlocal_cdrandextension_mappings), so the query threw and the card silently showed nothing whenever a specific server was chosen. The filter is now qualified to the CDR table. - Anomaly spike figures no longer double-count multi-scope extensions. An extension mapped both All-Servers (NULL) and server-scoped matched both rows in a flat join, doubling every CDR row before grouping and inflating call counts, cost and false volume spikes. The join now uses the canonical correlated-subquery form (server-specific mapping preferred over the All-Servers fallback), one row per CDR.
- The automatic updater relaunches the app after installing. After a verified download the app shut down to let the installer replace its files but never reopened. It now spawns a short-lived helper that waits for the app to exit, installs, then relaunches the freshly-installed app and cleans itself up.
Version 2.7.5 — 2026-07-19
Correctness release from the fourth full code review (high-effort multi-agent review of the v2.7.4 diff; 24 verified findings, all implemented). Schema v48 → v49. Supersedes v2.7.4, which was never uploaded — the migration fixes below ship in its installer, so deploy this instead.
⚠️ Report numbers may shift
Several fixes correct figures users may have reconciled against earlier versions:
- Department budgets follow one rule everywhere (Department report header and rows, Daily Summary, Budget Alerts, Departments screen "(Auto)" figures): descendant budgets take priority over a parent's stored figure, and a mid-level department's budget over unbudgeted children now surfaces in rollups instead of vanishing. An explicit R0 budget counts as a real budget. Budget Used % can change wherever the old figures disagreed with each other.
- The comparison report's automatic previous period gains its lost final day (it ended at midnight instead of end-of-day), and the Daily preset now compares against the full previous day instead of a midnight-only instant — Period B totals increase accordingly.
- Multi-PBX attribution is server-scoped in every report: a PIN/extension code mapped to different departments per server now credits each server's calls to that server's own department in the Department, Daily Summary, Inbound and Comparative reports — matching the invoice and Call Detail totals (previously whichever mapping loaded first won). Code matching is also case-insensitive.
- Top Dialed Numbers and Longest Duration Calls pick the answered leg of a call chain (ANSWERED-first, like every sibling report) — a BUSY/FAILED leg with bogus nonzero talk time no longer decides a row's duration, date or ranking.
- Restored mappings whose department cannot be matched arrive unassigned instead of silently pinned to whatever department owns the backup's raw id.
Fixed
Data layer / migrations (schema v49)
- New v49 migration heals databases the shipped v48 skipped. The v48 case skipped BOTH mapping tables when either was missing but still advanced the schema version — a database with one surviving table stayed permanently duplicated and index-less. v49 re-runs the dedupe + unique-index creation per table, idempotently; correctly-migrated databases are untouched. The v48 case itself is also per-table now for direct upgrades.
- The startup orphan repair can no longer fail forever. Two orphaned mappings sharing a code (two deleted servers both mapping '1234') collided with each other on demote; the aborted statement rolled back innocent repairs too and the swallowed error recurred identically on every startup. The dedupe-then-demote sequence is now single-sourced (MappingScopeSql) and shared with server deletion.
Backup / restore
- One department translator for every restored table. PIN and extension mappings restored without the departments section previously kept the RAW backup department id — silently re-pointing every mapping at whatever department owned that id locally. They now translate backup id → department name → local id, the same way budget alerts do; department-scoped call policies name-translate before deactivating (an empty id map used to deactivate ALL of them); and dropped budget alerts are counted correctly in the warning.
- Overwrite restores warn before deleting cached call history. A restore whose backup does not contain a locally-configured server permanently deletes that server's cached CDR rows; the restore now lists those servers with row counts behind an explicit confirmation instead of a log line.
Reports and billing
- One disposition rule. Five sites compared dispositions case-sensitively (policy violations never fired on non-uppercase data; the Cost Verification audit dropped every call) while the rest of the app matched case-insensitively. All 25 comparison sites now share CdrDispositions.IsAnswered.
- Comparative per-user counts match the arbitration rules (case-insensitive, single pass instead of a per-PIN rescan of every leg).
- Trend Analysis treats the current week/month as in-progress on its final day too — a morning run on the last day fed a period missing most of that day's traffic into anomaly detection and the forecast.
Sync
- A finished sync can never be reported as failed. Post-sync housekeeping (demo cleanup, settings write, mode detection) runs best-effort after the status row is finalized; a transient failure there previously overwrote the success status and raised a failure event. The demo cleanup sweep is also gated on the demo flag instead of running on every idle scheduled poll forever.
Desktop/UI
- Clear Cache re-checks for a running sync after the confirmation dialog — the auto-sync scheduler could start a sync while the dialog sat open, and the wipe then destroyed its data and watermark mid-sync (TOCTOU).
- Extension Management and Phonebook get the cross-command busy gate their PIN/Department siblings received in v2.7.4 (a CSV/PBX import could start while a bulk delete's refresh was still running).
- Drill-down "filter could not be applied" warnings are sticky — the data load finishing late no longer wipes them with "Ready." before they can be read.
- Department CSV import masks PIN codes in error messages (raw dialable PINs surfaced in the import-result dialog).
Internal
- Shared components replace drifted copies: MappingScopeSql (dedupe/demote SQL), DepartmentBudgetCalculator (effective budgets), ScopedMappingResolver (server-scoped mapping arbitration — Billing and Call Detail now consume the same resolver as the shared chain owner), CdrDispositions, PinExportConfirmation (raw-PIN export warning), ScheduleStatusClear (license status), and the chart dialogs call ChartPngRenderer directly (forwarding wrappers deleted).
- Tests: 2,103 (1,648 Core + 455 Desktop; the 9 DevDbE2EVerification tests are live-dev-database checks and require CDR data in the reporting window).
Version 2.7.4 — 2026-07-18
Reliability and accuracy release from the third full-project deep review (same six-dimension method; v2.7.3 scored 8.0/10 and every finding of that round is fixed here). No schema change — the database stays at v48.
Fixed
Data layer / multi-PBX
- Server deletion no longer fails on shared extension/PIN codes. Deleting a server demotes its mappings to "All Servers"; when another deleted server had already parked the same code there, the v48 unique indexes aborted the whole delete — the second server became permanently undeletable. Colliding duplicates are now deleted instead of demoted (same fix in the startup repair, which is also per-statement resilient now and no longer silently skips its later steps after one failure).
- The v48 migration is fail-fast and finally tested. It previously swallowed every error and stamped schema v48 anyway, which could leave a database permanently duplicated AND index-less; a failure now leaves the version at 47 so the idempotent case re-runs cleanly. The table self-heal also creates the v48 indexes.
- Restore-Overwrite handles servers that are not in the backup. Their cached rows are removed inside the restore transaction — previously the id remap could abort the whole restore (UNIQUE constraint) or silently merge a decommissioned server's call history into a restored server that landed on the same id.
- A failed backup no longer destroys the previous archive. Backups build under a staging name and atomically replace the destination only on success.
- Budget-alert history survives a restore that skips departments (raw department ids are kept when the departments section wasn't part of the restore).
- Zero-record syncs complete properly: demo data clears, the demo-mode flag resets, and the sync-completed event fires — an idle PBX could previously leave demo mode stuck forever. Clear Cache also refuses to run while a sync is in flight.
- Post-sync reclassification tolerates PBX-vs-PC clock/timezone offset (a 27h skew allowance on the incremental window; PBX-ahead clocks previously left freshly synced calls permanently unreclassified).
Reports and billing accuracy
- PIN-filtered User Activity exports show the right money. The export/Detailed path filtered per-leg and kept only the cost-free authorization legs (~R0.00 exports while the preview was correct); it now uses the same chain-level filter as the preview.
- Remaining leg-vs-chain counting stragglers unified: Top Dialed Numbers (counts read ~2x on PIN PBXes and the PIN filter zeroed costs), Daily Summary hourly chart (bars now sum to Total Calls), Department report daily trends, Comparative per-extension/per-PIN breakdowns, Cost Analysis preview call count, and Call Detail department totals (each record now counts in exactly ONE department, PIN-first).
- Same-named departments under different branches each keep their own row in the Daily Summary department breakdown (it was keyed by name; a real department named "Unassigned" also collided with the synthetic bucket).
- Department report parent rows use the leaf-budget rollup — a parent's stored budget is its children's rollup, so "stored + children" double-counted and halved the displayed Budget Used %.
- Weekly trends get the same partial-period handling as monthly: the in-progress week compares week-to-date against the same day-span and is excluded from anomaly detection and the forecast (no more false "call volume drop" anomalies on mid-week runs).
- Longest Duration Calls attributes PIN calls to the PIN's user (the accountcode sat on a discarded auth leg after the chain collapse).
- The comparison report's automatic Period B never overlaps Period A (it is now the same-length window immediately preceding A; AddMonths(-1) overlapped multi-month ranges).
- Invoice rate column is billing-mode aware (per-minute/per-second invoices show "cost + X%" instead of an unrelated per-call rate that never reconciled with the rows).
- Structured department billing arbitration is server-scoped: a PIN/extension mapped to different departments per server bills each server's calls to that server's own department (previously first-wins across all servers).
PIN privacy
- Masked the two remaining raw-PIN leaks: the Department dialog's PIN-assignment dropdown and the Cost Analysis export header (PDF/Excel) — the report body was already masked. CSV-import messages mask too, and PIN/department CSV exports (which must contain raw codes for re-import) now warn before writing the file.
Desktop/UI
- Drill-down navigation messages arrive deterministically on the UI thread (shared dispatcher-posted helper replaces the last four thread-pool Task.Delay(200) sends, closing a double-dialog race), and drill-downs tell you when their filter could not be applied instead of silently showing unfiltered results.
- Chart/detail dialogs in Reports are owned by the active window via the dialog seam; the first-run wizard no longer crashes on a database error during the demo-data prompt; license status messages no longer blank mid-operation; PIN Management and Departments gained the cross-command busy gate; chart PNG rendering and clipboard access moved behind seams.
Testing
- 41 new tests (suite: 2,054 = 1,602 Core + 452 Desktop, 0 failures), including first coverage of the v48 dedupe migration, Clear Cache watermark reset, the Dashboard Sync Now pipeline (all-servers fan-out via the now-public ISyncInvoker seam), forced billing fallback (UsedFallbackQuery), ChainKeySql delimiter/cross-server collision behavior, restore/backup atomicity contracts, PIN-leak scans, and DI lifetime pins.
- Corrected a long-standing false belief: plain connections DO enforce foreign keys (the bundled e_sqlite3 compiles with SQLITE_DEFAULT_FOREIGN_KEYS=1); comments and a pool-hygiene test now document the real invariant.
Release engineering
- create-release.ps1 verifies the MSI's embedded ProductVersion against version.json (a stale MSI with -SkipBuild previously shipped as the "new" version and then failed to install everywhere) and blocks untracked files under src/ and installer/.
- TESTING.md stale counts/dead planning text removed; historical release tags fetched to all clones.
Version 2.7.3 — 2026-07-18
Data-safety and consistency release from the second full-project deep review (same six-dimension method as v2.7.2; scored 8.0/10 against the previous 7.5). Schema updates to v48 automatically; no user action needed.
Fixed
- Clearing a server's CDR cache no longer strands its history. Clear Cache also resets the server's sync watermark (in the same transaction), so the next sync re-downloads the full history from the PBX — previously the deleted data was permanently unrecoverable in-app because every reachable sync was incremental.
- Restore can no longer wipe your servers or duplicate your call history. The Overwrite restore's server wipe now runs inside the restore transaction (a failed restore rolls everything back instead of leaving the server credentials deleted); restored servers keep their original IDs and surviving cached CDR rows are re-pointed at them by name, so per-server dashboards stay populated and re-syncing cannot double the data. Servers without a CDR password restore cleanly.
- Structured department billing charges dual-mapped calls exactly once. A call matching two sibling departments (PIN in one, extension in the other) was invoiced on BOTH sections of the same parent invoice; sections now share one PIN-first owner resolution. Department billing also bills PIN calls whose authorization leg never connected (the trunk minutes silently vanished from invoices before).
- One canonical chain key everywhere. Report code grouped calls with a key that treated the empty-string linkedid FreePBX actually stores as a real value — every such call in a period merged into ONE "call" and a single department could absorb everyone's spend, while billing (which handled it correctly) disagreed with the reports. Chain keys are also server-scoped now, so identical Asterisk IDs from two PBXes can no longer merge into one call.
- Reports agree with themselves and each other. Filtering by a user's PIN shows their full costs (the per-leg filter used to drop the costed trunk legs → cost read ~0); headers and per-user tables agree on call direction; Cost Analysis "Top Users" counts calls, not CDR legs (~2x on PIN systems); durations and category counts are chain-based and reconcile with the totals; the Department report's budget header uses the same leaf-budget rollup as the Departments screen and budget alerts; the Comparative Period report credits each call to one department.
- Dashboards no longer double-count dual-scope mappings. A PIN/extension mapped both as "All Servers" and for a specific server (the documented fallback pattern) fanned every matching call out twice in Top Destinations / expensive-call lists.
- Sync Now with "All Servers" syncs every enabled server (it silently synced only the primary, leaving other PBXes stale on multi-server installs).
- Call policies evaluate per call, not per CDR leg — a ring-group call produced one violation per ringing extension — and business-hours policies no longer flag after-hours incoming calls as staff violations.
- Backups include the billing module. Billing rate cards, run history, scheduled jobs, invoice settings, the invoice number sequence, call policies and budget-alert history are now backed up and restored; the invoice sequence merges by highest number per year so a restored machine can never re-issue an invoice number already sent to a customer. Same-named departments under different branches restore correctly (the old name-only matching merged them or aborted the restore).
- Backup tampering is detected. Encrypted (v2.7.2+) archives must contain encrypted payloads — a backup whose contents were swapped for plaintext data is refused instead of silently applied. Backup payloads are also encrypted the moment they are written (no plaintext license token / PIN window in the temp folder), and a restored license token is encrypted at rest immediately.
- Demo-data cleanup only deletes demo data. The old range matching deleted real PINs like 10050 and real extensions 100-131 along with the demo fixtures.
- Smaller fixes: duplicate "All Servers" mappings can no longer accumulate after server deletions (deduped + unique index, schema v48); sync watermarks are read culture-independently (Thai/Arabic locale calendars mis-parsed them); boundary-second CDR rows are no longer deferred indefinitely on idle PBXes; a sync whose rows all fail to parse no longer advances the watermark to the client clock; the Internal Calls report skips (instead of crashing on) chains with no source number; rapid filter changes on Policy Violations / Budget Alerts can no longer show stale results; a double-click can no longer start two report generations; billing exports are disabled while a generate runs; a failed policy toggle reverts in the grid; PINs are masked in the Reports filter dropdown and in duplicate-mapping error messages.
Development
- Test suite grows to 2,013 tests (1,575 Core + 438 Desktop, all green). New behavioral coverage for the auto-sync scheduler pipeline (via an internal sync seam), backup tamper-rejection/rollback/id-remap contracts, and billing's primary query (a fixture now produces a result only the primary query can produce, and a test asserts the silent fallback did not run — the v2.7.1 escape mechanism is closed). The schema drift test now compares column types, NOT NULL and defaults (it immediately caught two real fresh-vs-upgraded divergences, now reconciled). Performance timing assertions are advisory unless CDR_PERF_ASSERT=1. TESTING.md rewritten to match reality (the fabricated CI/CD section is gone); installer/README.md rewritten (bundled WiX 3, real scripts and paths); the dead installer .wixproj (which built v1.0.0 MSIs without the uninstall dialog) is deleted; the versioned LICENSE.rtf copy is written BOM-less.
Version 2.7.2 — 2026-07-17
Accuracy and reliability release from a full-project code review (six review passes: reports, UI lifecycle, security, billing, data layer, tests). Schema updates to v47 automatically; no user action needed.
Fixed
- Every screen now counts calls the same way. All reports previously loaded their call data with different internal caps (50k/100k/500k rows), so on large databases two reports could silently analyze different call populations — the Department and Daily Summary reports disagreed by thousands of calls on a real PBX. Every report now shares one 500,000-call cap and warns when a date range exceeds it instead of silently dropping the oldest calls. User Activity, Personal/Business, Comparative Period and Trend Analysis also now count one call per call chain (like the Daily Summary and Department reports), so a PIN-dialed call is no longer counted twice.
- Budget alerts now fire correctly on PIN-based systems. Department spending (dashboard tiles and budget alerts) used a row-level match that missed the costed leg of PIN-dialed calls entirely — a department could overspend forever without a single alert — and could double-charge a call to two departments. Both now use the same chain attribution as the reports. Department rollups also work again on databases restored from older backups.
- "Auto-sync every N minutes" now actually runs. The interval sync timer was never started in practice (and died on screen navigation when it was). It now runs app-wide regardless of the open screen, and settings changes take effect within a minute — no restart needed. Manual "Sync Now" and auto-sync coordinate safely.
- Billing corrections. One phone call is billed once even on PIN-dialing systems (previously the authorization and trunk legs could each be charged in Per-Call mode); internal extension-to-extension calls are never billed; department billing no longer under-charges cost-based modes on PIN-only systems; phonebook numbers match in both national (0...) and international (27...) forms so calls stop dropping off invoices; QuickBooks export lines always balance (quantity x rate = amount); invoice numbers can no longer duplicate when a scheduled run and a manual invoice coincide; deleting a PBX server deactivates its billing schedules (orphaned schedules used to email empty invoices every month).
- Dialogs no longer open twice after navigating between screens (stale screens kept listening for navigation messages); rapid server/period switching can no longer show a slower, older query's data over the newer selection; report generation buttons are disabled while a generation is already running.
- Database upgrade safety. A failed upgrade of the PIN/extension mapping tables now rolls back and retries on next start instead of silently continuing with the data stranded (and the version wrongly stamped); departments whose names differ only by case no longer brick the v43 upgrade; upgraded databases regain several indexes lost in old table rebuilds (migration v47) — a new automated test now diffs a fresh install against a fully-upgraded database to keep them identical; databases restored from backups get their department hierarchy paths rebuilt on startup.
Security
- Backup archives are now fully encrypted. Previously only the two server passwords were encrypted — the rest of the archive (including the license activation token and raw PIN codes) was readable by anyone holding the file, and the automatic-backup password was derivable from information stored inside the backup itself. Auto-backups are now protected by a machine-stored secret; manual backups use a per-backup salt with a stronger key derivation and require a 12+ character password. Existing backups still restore.
- Update downloads are only accepted from the official q5000.co.za server — a spoofed update response can no longer point the downloader (or browser fallback) elsewhere.
- The license activation token and license key are encrypted at rest, and remaining raw-PIN log lines were masked (completing the v2.4.0 PIN-privacy rule).
- A rare startup path could wipe stored PBX passwords while a credential migration was still pending retry; it now waits for the retry instead.
Changed
- Faster syncs on large databases (the CDR unique index is no longer rebuilt on every sync); reclassify with no rate file loaded now degrades gracefully instead of failing; Trend Analysis accepts an exact 4-week range; mobile/destination classification uses the raw dialed number instead of phonebook display names.
- Test suite grew from ~1,864 to ~1,975 tests with the previously untestable license/email/AI/update network layer now covered (including the v2.7.1 license re-validation flow), the setup wizard tested for the first time, and the suite's main flakiness sources removed.
Version 2.7.1 — 2026-07-16
Reliability fixes for startup, first-run and licensing. No schema change.
Fixed
- Start with Windows now tells the truth. On a locked-down machine the toggle no longer reports a false success when the change did not take; it respects Task Manager's Startup "disable" flag (the checkbox reflects whether Windows will actually launch the app); it repairs its startup entry if you reinstall Q5000 to a different folder; and uninstalling now removes the entry instead of leaving a dead auto-start behind. (Also restores a long-missing "delete my data?" prompt on uninstall that had been silently dropped from the installer.)
- First run no longer opens an empty window after activation. Activating a license on a fresh install now flows straight into the setup screen (Restore a backup / Connect a PBX / Upload a CSV / Demo mode) instead of requiring a restart, and that screen is enlarged so all four options are visible without scrolling.
- Restoring a backup no longer loses your license. When a restored backup (or a long offline gap) trips the validation grace window, Q5000 now silently re-validates your stored license instead of forcing you to re-enter your key — with a one-click "Validate Now" fallback when it cannot reach the server.
Version 2.7.0 — 2026-07-15
Adds a Start with Windows option. No schema change.
Added
- Start with Windows. A new "Start Q5000 automatically when Windows starts" option under Settings -> Sync (the Startup card). When enabled, Q5000 opens automatically each time you sign in to Windows; when disabled, it does not. The setting is per Windows user account (registered under the current user's startup entries) and requires no administrator rights. On login the app opens as normal. The checkbox always reflects the actual registered state, and if a locked-down environment prevents the change from taking effect the app shows a warning rather than a false success and leaves the setting unchanged.
Version 2.6.2 — 2026-07-14
Adds a dedicated Internal Calls report. No schema change.
Added
- Internal Calls report. v2.6.1 split internal (extension-to-extension) calls out of
"Outbound", but that left them visible only as sub-sections inside the Call Detail and User
Activity reports. This new report treats internal traffic as the subject, in four sections:
- Summary tiles - total internal calls, total talk time, average call length, longest call.
- Longest Internal Calls - the longest ext-to-ext calls ranked by talk time, with calls over ten minutes highlighted.
- Internal Calls by Extension - calls made, calls received, total, and talk time per extension, so you can see who the internal traffic flows to and from.
- Top Extension Pairs - the extensions that talk to each other most, both directions combined (201 to 205 and 205 to 201 are one row).
- Internal calls carry no cost and no PIN, so attribution is by extension number and name. A chain counts as internal only when it originated from one of our extensions and never reached an external number, so inbound calls (which also ring an extension) are correctly excluded. Found under Reports -> Internal Calls.
Fixed
- Clipped column in exported Excel reports. Labels containing an em dash - such as an extension shown as "219 — Despatch PTA" - could be clipped in the spreadsheet because the column auto-fit under-measured the character. Every report's Excel columns are now widened to fit their longest content.
Version 2.6.1 — 2026-07-13
Reporting-accuracy follow-up to v2.6.0. Internal calls now have their own direction throughout the per-user reports instead of being lumped into "Outbound". No schema change.
Fixed
- Internal calls were counted as "Outbound" in the per-user reports. The reports decided
direction from the call's source (anything originated by a known extension or PIN was
"outbound") while the rest of the app — the dashboard, the SQL filters, and the sync-time
classifier — decides from the destination. So an extension-to-extension call (e.g. 226 → 227,
no trunk, no cost) or a feature code (
*8) showed up under "Outbound by User". Direction is now three-way: a chain is Outbound only when a leg actually reached an external number, Internal when it stayed between our extensions/feature codes, and Inbound when the source is external.- PIN/Mixed sites gain a third Internal by Extension section in the Summary, User Activity and Call Detail reports (preview, PDF and Excel).
- Extension-only sites gain an Internal column so "Outgoing" no longer double-counts internal traffic.
- The Summary header shows a distinct Internal total next to Inbound and Outbound, and the three reconcile.
- Top Expensive / Outbound Spend by User now ranks calls that reached an external number only, so zero-cost internal calls no longer pad the list. PIN attribution is unchanged (the PIN owner is resolved from the whole chain before the external-only filter).
Version 2.6.0 — 2026-07-12
Reporting-accuracy release, built from a full end-to-end drive-through of v2.5.0. The headline fix is per-user call attribution on PIN-dialing PBXes; alongside it are a P1 duplicate-mapping fix, a new server filter, and a handful of accuracy/polish items. Schema v46 (one additive column). Test suite ~1,784.
Added
- Call Identifier Mode now drives report layout. On PBXes that use PIN/account codes for outbound calls (mode PINs-only or Mixed), the per-user reports split into an Inbound by Extension section and an Outbound by User section — Summary, User Activity and Call Detail daily totals; Top Expensive is labelled "Outbound Spend by User". Extension-only sites keep the classic combined layout unchanged. Previously this setting was detected but never changed any report.
- Server filter on Extension Management and PIN Management. Reuses the dashboards' selector; choosing a server shows that server's mappings plus the All-Servers ones (what actually takes effect on that server) and composes with the search box.
Fixed
- PIN-authorized outbound calls were attributed to the dialing extension. On PIN PBXes the
authorization and trunk legs are separate CDR rows sharing a linkedid; the old per-leg logic
picked the answered trunk leg (which carries no account code) and credited the extension —
so an extension that cannot dial out showed outgoing calls, and the same person appeared
twice (an extension row and a PIN row). A new shared
ChainUserAttributionresolves the whole chain PIN-first; Summary, User Activity, Call Detail daily totals and Top Expensive all use it. - Duplicate extension/PIN mappings after backup restore or PBX/CSV import. Backups never recorded which server a mapping belonged to and restored everything as "All Servers"; imports then checked only same-server rows, so every existing mapping was re-imported as a server-specific duplicate (reported as zero skips) and could trip the license cap. Backups now preserve server scope (old backups restore as All Servers), restore remaps server ids by name across machines, and imports skip a mapping that already exists in any scope.
- AI executive summary sent the totals row as a user and read column keys that no longer existed, so every user line came through blank. It now excludes the TOTAL row and uses the real preview fields.
- AI Lookup crashed on numbers already in the Phonebook ("AI lookup failed") because the add path throws on duplicates. It now updates the existing entry, keeping its name and category and recording the AI suggestion as a note.
- Department Report header "Budget Used %" was pro-rated to the report window while the rows below used the full monthly budget; the header now uses the full monthly budget too (and the day-count off-by-one is gone).
- Last-sync time looked stale after a sync that found no new records. A new
last_success_atcolumn records the wall-clock completion time (written on every successful sync, including zero-record ones); the incremental watermark is untouched.
Changed
- Inbound dashboard header buttons (Open Full Report, Refresh) use the app's outlined styles.
- The bulk "Identify Unknown Numbers" dialog asks "Open Phonebook now?" on all three screens, and the Inbound one now offers it (was info-only).
- Outbound dashboard help text follows the configured currency name instead of hardcoding "Rands".
- Inbound vs Outbound and Top Dialed report descriptions note their counting population, which explains small differences from chain-based reports.
Version 2.5.0 — 2026-07-12
Quality release built from a week of systematic verification: two full-app review rounds, a complete report audit, and dedicated sweeps of every Excel workbook, every PDF export, every report filter, and the entire billing pipeline — every finding fixed and live-verified against real CDR data. The three user guides now ship with screenshots. Under the hood, the test suite grew from ~1,160 to 1,741 tests (measured coverage 32.8% → 54.8% line, Core 72%) and all 13 screen ViewModels moved onto injectable UI seams, which surfaced and fixed several latent crashes — including one that broke Backup for any Demo Mode install.
First full-app review round (2026-07-10): all 18 findings fixed (4 critical, 6 major, 8 polish).
Fixed
- Call chains grouped incorrectly across the app — the main CDR query omitted the
linkedidcolumn, so multi-leg calls (ring groups, transfers) could not be grouped into chains; linkedid chain grouping is now the canonical model everywhere. - Reports using rate lookup could run before rates loaded (
LookupRatecallers now ensureEnsureRatesLoadedAsync), and the Reports grid's "View" template column blanked non-total cells. - Introduced shared
ChartPalette,InvariantPlotModel, andExportPathHelper(export dialogs remember the last used folder), and anOutlinedOnDarkButtonstyle for dark dialog footers; assorted DPI/layout fixes.
Second full-app review round (2026-07-10, 4K/300% DPI): all 26 findings fixed — 6 correctness (inbound classification, chain grouping) and 20 polish/consistency items.
Fixed
- Inbound vs Outbound report missed trunk-name CDRs. Its inbound test only looked at the
srcshape, so PBXes recording the trunk name insrc(e.g. "JHB") with the real caller incliddumped nearly all inbound traffic into Internal/Other (124 vs 1,002 on live data). A new sharedPhoneNumberNormalizer.ExtractCallerNumbermirrors the SQL-side clid extraction and is used by the report, the Call Details drill-down pie (which showed one "JHB (100%)" slice), and the Longest Duration report. - Inbound Dashboard Service Level was a leg-based artifact (~100% always). Per-leg ring time credited the IVR's instant pickup. SLA is now chain-based on the same arrival-to-human-answer model as the Avg Hold tile, so the two tiles finally agree.
- Avg Hold Time sampled outbound chains. The chain population is now restricted to inbound chains (the tile could show more samples than Total Inbound on the same screen).
- Comparative Period counted ring-group legs as calls. Total/Answered/Missed are now chain counts (a 3-leg answered chain was 1 total + 2 "missed"); costs remain per-leg sums.
- Summary Report user rows counted legs while the header counted chains. Per-user Incoming/Outgoing now attribute each chain once to its answering user; the top-N TOTAL row is labelled as a subset ("Top N of M users").
- Dead "View" text in Summary-mode previews. Non-TOTAL rows rendered the internal "View" marker as an unclickable link-looking text; the marker is now suppressed.
- Department Report rows did not sum to the TOTAL. Calls whose owner resolves to no department (e.g. mapped extensions never assigned to one) now get a visible "Unassigned" row.
- Top Expensive Calls TOTAL mixed populations (top-N call count against all-calls cost). The row is now a self-consistent top-N subtotal; the header keeps the all-calls totals.
- Call Detail daily totals showed raw extensions. User keys now resolve through the extension mappings ("Alice (201)"); the count column is titled "Calls Made" to reflect that it counts originated calls.
- Trend Analysis rendered call-volume anomalies as currency ("R87.00" for 87 calls) and the Latest Month tile hardcoded "R"; both are metric-aware/currency-configured now.
- Chart locale mixes on comma-decimal locales. OxyPlot's PieSeries ignores the plot Culture
for slice labels (verified against 2.1.2 source) — a new
InvariantPieSeriesfixes all 13 pie sites; LiveCharts y-axes without aLabeler(busy-hour, daily-trend, missed-by-hour) now format ticks invariantly to match their bar labels. - AI anomaly card contradicted itself ("increased from 243 to 213 (extrapolated to 639)"). The probe now uses full-day windows only (today's partial day inflated the per-day averages) and every payload line states the volume direction explicitly (rose/fell from X/day to Y/day).
- Budget Forecast card had no "as of" date. Now appends the same "Generated {date}" footer as the AI Insights card on both fresh and cached paths.
- First department budget group had no header. The alphabetically-first branch (e.g. Jetpark) now gets its group title like every other branch.
- Layout at high DPI: Recent Calls / Top Reports headers rebuilt on real grid columns (the two overlaid panels collided at 300% scaling); the Policy Violations filter bar wraps instead of clipping "Identify Unknown Numbers"; Billing name/Filter columns have minimum widths (they collapsed to ~2 characters); the server selector no longer shows a dangling "- " when a server has no location (Reports, Violations, Inbound).
- Chart dialogs now close on ESC (Top N, Department Breakdown, Personal vs Business, Comparative, Inbound vs Outbound, Call Details) and the Top N window title matches its header ("Top N Users Breakdown").
Changed
- Longest Duration Calls now includes inbound calls with a Direction (In/Out) column — previously outbound-only despite the description promising the longest calls overall.
- Settings PBX Servers list auto-sizes (max 300px, min 120px) instead of a fixed 300px box that was mostly empty with one server.
- Cost by Category pie caps at 9 slices + "Other (n)" via a shared helper — unbounded slice counts made the outside labels overlap unreadably.
Full report audit: verified every one of the 13 report types produces correct results per its description and that every filter the UI exposes actually reaches the generator, in both Summary and Detailed view modes. Added 62 tests (report suite 55 -> 117 cases; 0 failures). Live-verified in the running app against real client CDR data.
Fixed
- Stale filter leak (systemic).
ReportFilterState.ToReportFilterscopied every filter value into the report regardless of whether that control was visible for the selected report type, so a value set on one report (e.g. Min Cost on Call Detail) kept restricting results after switching to a report that hides the control. Every emitted value is now gated on its visibility flag. - Stale view-mode leak. Switching away from Trend Analysis (which is forced to Detailed) left the
next summary-only report stuck in Detailed, force-showing Top N / Destination Pattern and an
unreachable "Detailed view" note. Summary-only reports are now pinned to their fixed mode on
selection; the summary-only set is a single source of truth (
ReportsViewModel.IsSummaryOnlyReport). - Department report ignored its Call Category filter. The report exposes a Category filter but only scoped by department -- the category selection had no effect. Now applied.
- Cost Verification misclassified phonebook-named destinations. Rate lookup and internal/external
classification used the display-resolved
Dst, so an external number matched to the phonebook was read as internal and dropped from the audit (or flagged as missing a rate). Now uses the rawOriginalDstthroughout the analysis. - Comparative Period ignored the Extensions filter and scoped only its breakdown rows. The
headline summary metrics aggregated over every call, ignoring the Departments and Extensions
selection; extension grouping matched the resolved
Srcname; sub-department rollup was one level deep. Both periods are now scoped by the visible filters before the summary is computed, matching on rawOriginalSrc, honoring Include-sub-levels to full depth.
Changed
- Cost Verification: Cost Range filter removed. A minimum-cost floor would empty the report's zero-cost buckets (toll-free, sub-second, truly-missing) and mask missing rates, so the control is hidden rather than half-applied. Its description was also tightened to match what the report does.
- Inbound vs Outbound now reports the metrics its description promised. Added answer rate, average talk time, and cost split per direction (previously call counts only) to the summary, PDF, Excel, and preview.
- Removed the unreachable "Detailed view" notes from the four summary-only reports whose view-mode toggle is hidden (Comparative Period, Cost Verification, Inbound Analytics, Inbound vs Outbound).
Excel export sweep (2026-07-11): all 20 report workbooks verified cell-by-cell against the database.
Fixed
- Daily Summary Excel silently truncated large periods (the generator queried with a low row cap; now 10,000) and one workbook leaked raw PIN codes — masked like every other surface.
- Department rollups skipped departments with a NULL hierarchy path (pre-v34 rows); the rollup CTE now handles them.
Changed
- All workbooks share one branded template: consistent header/branding, sensible column widths, tab names, auto-filter on data sheets, and true duration formatting.
PDF export sweep (2026-07-11): every report PDF reviewed page by page.
Fixed
- Personal vs Business detailed PDFs omitted their chart, and several PDFs formatted numbers with the OS locale instead of invariantly.
- Daily Summary detailed PDFs cap at 500 rows with an explicit truncation note; User Activity honours the Max Users setting; the extension-comparison section no longer counts trunk legs.
Changed
- Money renders as "R1,234.56" suite-wide via a single
CurrencyHelper.FormatMoney; every PDF footer carries the same "Q5000 - Confidential" text; charts embed larger and sharper; report sections keep together across page breaks; hierarchy breadcrumbs use " > ".
Report filter sweep (2026-07-11): all 13 reports driven live in both view modes; 12 findings fixed.
Fixed
- Top N pie charts covered fewer users than the tiles/table — they now chart the full Top N with an "Others" bucket past 15 slices, and the Top-N table appears in the PDF.
- Clearing a numeric filter box no longer leaves a stale value applied (
TargetNullValue), Trend Analysis re-pins its forced Detailed mode correctly, and department budget rollups in the filter panel match the Departments screen.
Changed
- Detailed-mode filters are now opt-in per report type — a filter only appears where the report actually honours it.
Billing sweep (2026-07-11): the full pipeline re-verified — generate, invoice, Excel, PDF and accounting export all reconcile to the cent on live data.
Fixed
- Fixed charges could double-count. Charges are now attach-only: line items carry them and the grand total adds them exactly once; an "Each" charge is per client/department entity, never per user row.
- The billing To-date excluded the final day (now inclusive), and the accounting CSV formats amounts plain F2 (a thousands separator was splitting values across columns).
Changed
- Billing and profit exports moved onto the shared report branding (the invoice layout stays deliberately bespoke), and invoices show a VAT-inclusive banner.
User guides and dialogs.
Added
- The three user guides now include 17 annotated Demo Mode screenshots with a full
page-by-page layout polish (
generate_pdfs.pygained image, H4, and list-formatting support).
Changed
- The last eight Windows-plain dialogs restyled onto the Material house patterns; the dead Email Settings dialog was removed (email has gone through the relay since v2.2.0).
Reliability fixes surfaced by the new test suite.
Fixed
- Backup crashed on any Demo Mode install (and restore had the mirror bug): departments
with no cost centre or manager email — which the demo data always creates — aborted
CreateBackupAsyncwith a NULL-column read. Budget Alerts had the same NULL crash. - Trend Analysis crashed when recent months were flat (average over an empty change-sequence).
- Full test runs could report "Test Run Aborted" despite every test passing:
LocalDatabase.Disposenow clears its SQLite connection pool, releasing file handles promptly (this had also leaked 90,000+ temp databases on dev machines).
Internal
- All 13 screen ViewModels construct headless through internal DI seams
(
IDialogService/IUiDispatcher/IFilePickerService) — no behaviour change, but the screens' command logic is now unit-tested (guarded deletes, validation chains, exports). - Test suite: ~1,160 → 1,741 tests (0 failures, both projects exit clean); measured line coverage 32.8% → 54.8% (Core 72%, repositories 94%). Demo Mode doubles as a seeded, deterministic export-test fixture; generated Excel/PDF files are re-opened and verified.
- Refactoring extractions:
ReportPdfStyleHelper(shared PDF styling),AiCardCache, Reports chart-PNG/AI-context builders, a shared billing export shell; CLAUDE.md trimmed to load-bearing conventions; TESTING.md rewritten against measured numbers.
Version 2.4.1 — 2026-07-05
Patch release fixing everything found in the post-v2.4.0 final review sweep, which targeted the surfaces no v2.4.0 batch owned (report chart dialogs, backup-restore dialog, shell).
Fixed
- Dashboard drill-down logged raw PINs. The
CostAnalysisNavigationMessagehandler in Reports logged the account code unmasked (three sites) — the only path the v2.4.0 PIN log masking missed. Now masked with the samePinDisplay.Maskform as everywhere else. - Hardcoded "R" currency in chart dialogs. The Top N Users and Department Breakdown chart dialogs' Total Cost tile showed a literal "R" instead of the configured currency symbol (rate-file currency selection, v2.3.4). Both now use the app-wide currency converter.
- Flaky integration test.
Integration_Should_Support_Department_PIN_Extension_Flowfailed ~1 in 10 runs: the PIN mapping'sIsActivewas left to the Bogus faker (90% true) while the repository query filters active-only. Pinned explicitly, same as the extension mapping fix before it.
Changed
- Emoji/text glyphs replaced with Material icons in the seven dialogs the restyle missed: the six report chart dialogs (Top N, Comparative, Inbound vs Outbound, Department Breakdown, Personal vs Business, Summary Detail — text "✕" close buttons, "📄 Export PDF") and the backup-restore conflict dialog ("âš ï¸"/"✓" markers). Their grids and PDF exports also format call counts and percentages with invariant culture now ("100,0%" no longer appears on comma-decimal locales — 20+ sites), completing the v2.4.0 locale-safety sweep.
Removed
- Dead Cost Analysis screen (~1,000 lines).
CostAnalysisView/CostAnalysisViewModelwere never reachable — the dashboard drill-down they served is handled by the Reports screen (Top Expensive Calls) since v2.1.x. The Cost Analysis report is unaffected. - Empty
CDRReporter.Integration.Testsproject (its tests moved toCore.Tests/Integrationlong ago); it only added build and test-discovery time.
Version 2.4.0 — 2026-07-05
Major feature and polish release: scheduled report email delivery, working scheduled billing (schema v45), a shared dashboard period selector, PIN privacy, two new reports, a report-filter audit that fixed every silently-ignored filter, an app-wide locale-safety sweep (400+ formatting sites), and a Material Design restyle of every remaining screen.
Added
- Scheduled reports (automatic email delivery). Any saved report template can now be generated in the background and emailed on a Daily / Weekly / Monthly schedule (new Schedule... dialog in Reports). Reports always cover the period that just ended; missed occurrences catch up shortly after the next app start; delivery goes through the Q5000 relay (requires an activated license). This feature was documented since v1.6 but never existed.
- Working scheduled billing jobs. The billing scheduler was never started and its dialog couldn't configure a filter, so no billing schedule ever ran. Rebuilt end-to-end: the service now starts with the app, catches up missed occurrences, guards against overlap, writes a billing-history row for every successful run, and sends a short notice email instead of an empty PDF when a period has no billable calls. The job dialog supports billing type (Department / User / Client), an optional department/category filter, day-of-month, editing, and an Active toggle. Migration v45 deactivates pre-v2.4.0 jobs on upgrade so broken legacy schedules cannot surprise-fire - recreate the ones you want.
- Dashboard period selector. Both dashboards share a Monthly (month dropdown incl. "Last 30 days") / Weekly (snaps to Monday-Sunday) / Daily / Custom selector. It drives the Inbound Dashboard entirely (hourly charts switch to per-day averages for multi-day periods) and, on the Outbound Dashboard, the summary tiles, Department Budget Status (calendar months only), and both Busy Hour charts - which previously ignored the date filter altogether.
- PIN privacy. Raw PIN/account codes are never displayed anywhere - dashboards show Extension (number) and User (name) columns, reports print user names, and an unmapped PIN appears masked (e.g. "PIN ..23"). Clicking a masked PIN on the dashboard opens PIN Management with the Add dialog pre-filled. PINs are also masked in logs.
- Suggest Departments on PIN and Extension Management. A free deterministic name cross-match (user names vs department names, both directions) proposes assignments in a preview dialog - Exact matches pre-ticked, Likely/Possible unticked, ambiguous names excluded. On Extension Management, AI destination-pattern analysis is offered only afterwards, for still-unmapped extensions with recent call activity.
- Inbound Dashboard: Service Level tile (% of inbound calls answered within 15 seconds), joining the abandonment metrics row.
- Call policy scope ("Applies To"). Policies can target All calls, a specific department (picked from the hierarchy), or a specific extension; the rule summary shows the scope and editing preserves it. Deleting a scoped department/extension deactivates the policy instead of letting it silently go global. Policies also gained a one-click Active toggle in the grid.
- Violations "Scan Now". Scanning is now an explicit button that reports how many new violations were found; the From-date and Show-acknowledged filters apply immediately instead.
- Phonebook category manager rework. Shows managed and in-use categories (unmanaged categories appear as chips and are adopted on first edit); renaming onto an existing name offers to merge entries. Import gained an update-existing mode; category colors show as dots in the grid.
- Department hierarchy tree search that keeps each match's ancestors visible, plus CSV round-trip carrying hierarchy, budgets, and PIN/extension assignments.
- PIN CSV round-trip with Department and Server columns (match-existing-only; unknown or ambiguous names are reported, never guessed). Extension and Department CSVs share the same RFC-4180 engine.
- Budget alert hierarchy rollup. Alerts now cover departments tracked by extensions as well as PINs, roll spending up the N-level hierarchy, and alert on parent branches whose budget is the sum of their children ("(Auto)" branches) - matching what the Departments screen shows.
- Top Dialed Numbers report. New report ranking the most frequently dialed outbound numbers over the selected period, with call count, total duration, total cost, and last-called time per destination. Detailed view expands each top number into its individual calls. Supports Department, PIN, Extension, Category, destination-pattern, and Top-N filters.
- Longest Duration Calls report. New report listing the longest outbound calls by billable duration, with user attribution (PIN or extension), source, destination, cost, and category. Supports the same filters plus a duration range. Summary-only (it is inherently a per-call list).
- "Open Full Report" buttons on the Outbound Dashboard's Top Reports card. Each tab (Most Expensive, Longest Duration, Top Destinations) now has a quick-open button that jumps straight to the matching report in the Reports view — Top Expensive Calls, Longest Duration Calls, or Top Dialed Numbers — with that report's default date range applied.
Fixed
- Extensions could rank as top outbound destinations. (Client-reported: extension "009" —
Reception — ranked #1 in Top Dialed Numbers with 2,966 calls.) The outbound test treated any
number starting with
00or9as external, which swallowed extensions and short internal codes that happen to share those prefixes. A dialed number that matches a configured extension is now always internal, and prefix-based matches require at least 6 digits (short queue/feature codes no longer count). Applies to the shared dashboard outbound rule (Today / This Month tiles, Top Reports lists, busy-hour and trend charts) and the two new reports alike. - Personal vs Business report ignored the PIN, Extension, and Category filters. (Client-reported: selecting a single PIN in Detailed view still previewed every user.) The generator only ever applied the Department filter; selected PINs, extensions, categories, and the Detailed-mode destination pattern now all filter the call set — in the preview, PDF, and Excel alike.
- Summary Report ignored the Department and Category filters. The daily summary always covered every department regardless of selection; department scope (including sub-levels), category, and destination-pattern filters now apply, and the previous-day comparison uses the same filtered scope so it stays like-for-like.
- Top Expensive Calls report ignored the Department filter and matched a selected extension against the display-name-resolved source/destination, missing calls whose extension was mapped to a user name. Department scoping now applies, and extension matching uses the raw numbers.
- Cost Verification & Audit report ignored the Department filter. Department scoping (including sub-levels) now applies before the rate checks.
- Trend Analysis report ignored the Department and Category filters. Trends, anomalies, and forecasts are now computed only from the in-scope calls.
- Inbound Analytics report ignored the Extension, Department, and Ring Duration filters.
Selected extensions (a department selection resolves to its mapped extensions) and min/max
ring-time bounds are now pushed into the analytics queries — including the ring-group case
where the answering extension only appears in
dstchannel, which a plain destination match could never catch. Dashboard tiles are unaffected. - Extension filters matched display names instead of raw numbers in several reports.
User Activity, Call Detail Export, Comparative Period, and Inbound vs Outbound matched the
resolved
Src/Dst(whichCdrServicerewrites to user/phonebook names) instead of the rawOriginalSrc/OriginalDst— the same defect class as the Department report fix below. All extension/department matching now goes through a sharedCallReportScopeHelperusing the raw numbers. - Comparative Period report compared every active department regardless of selection. With departments selected, only those departments are compared; with none selected the previous all-departments behaviour is kept.
- Department report could list employees from other departments. (Client-reported.) A call
was pulled into a department-filtered report if any of its identifiers touched the selected
departments (PIN, source extension, or destination extension), but the per-user breakdown
then attributed the call PIN-first/source-first against all mappings — so an internal call
from another branch into a selected department showed up under the caller's name (e.g. a
Jetpark employee in a Silverton report). Attribution is now scoped to the selected departments
(including sub-levels): such a call is credited to the in-scope receiving employee, and a
user from an unselected department can never appear in a department-scoped report. Shared
ResolveCallOwnerhelper now backs the user breakdown, department breakdown, and per-department Top-N chart. - Department breakdown double-counted cross-department calls. In All/multi-department mode each department re-matched every call independently, so one internal call could appear under both departments' pie slices (slices summed past the report total). Each call is now counted once, under its resolved owner's department; in multi-select mode unselected departments no longer get a slice at all.
- Source-extension matching in the Department report never worked for mapped extensions.
CdrServiceresolvessrc/dstto display names for the UI, but the report matched the resolved source against extension numbers — so a mapped extension's outbound calls only entered the report via PIN or destination. Matching now uses the raw source number (OriginalSrc), same as the destination side already did. - Month-to-date comparison ignored sub-departments. The previous-month comparison in the single-department report rebuilt its filter from the exact department only; it now uses the same sub-level-expanded scope as the main period, so the comparison is like-for-like.
- AI phone lookup was silently dead. The lookup service received a fresh AI client whose daily limit was never refreshed, so availability was permanently false and single/bulk lookups returned nothing without logging (regression from the v2.2.0 composition root). The q5000.co.za proxy allowlist was also missing the 8 template keys added since v2.1.2, which had broken Inbound AI Insights, Budget Forecast, Security Alert, and Suggest Departments.
- Locale-safety sweep (400+ sites). All report generators, previews, billing PDF/Excel exports, dashboard tiles, VM-built display strings, and AI prompt payloads now format numbers and dates with invariant culture - comma-decimal Windows locales (e.g. South African) no longer produce "R251,04"-style output or corrupt stored values. Department budget inputs, Settings rate inputs, and the money fields in dialogs use a comma-safe decimal parser ("1500,50" is no longer read as 150050).
- Avg Ring on Top Missed Extensions was wrong in both directions. Queue retries summed into one extension (12:56 shown for one attempt) while any answered leg zeroed others; ring time is now the longest single attempt per extension and call chain.
- Budget alert history filters were either/or. Department and date-range filters now combine, and "All Departments" no longer queries a non-existent department and empties the grid.
- Violation scanning fixes. The purge window can no longer outrun the scan cap (old unacknowledged violations survive rescans), department-scoped policies match the call's account code rather than its source, the per-row AI/Google lookup buttons work (they were bound to a property that didn't exist on violations), deletes ask for confirmation, and acknowledging a known number no longer prompts to add it to the phonebook.
- Invoices could be Issued with no document. Issued status and the invoice number are now committed only after the PDF is written to disk; generating a tax invoice with empty company details warns first.
- Settings screen defects. The PBX Servers "Records" column always showed 0 (now the real per-server count); Save Configuration was a silent no-op (now saves the tab and applies changed Quick Rate Adjustment values); the Add Server dialog clipped its second option; banner texts clipped instead of wrapping.
- Scrolling over a closed dropdown no longer changes its selection (app-wide fix; wheel scrolling over the Reports type combo used to silently switch report types).
- Log filenames were double-stamped (
cdr-reporter-2026070420260704.log); fixed to the documentedcdr-reporter-YYYYMMDD.log, with a one-time cleanup of mangled files. - Inbound Dashboard header overlap. The Open Full Report button no longer paints over the AI/Google legend on the Top Inbound Callers card at narrower window widths.
Changed
- Material Design restyle completed across the app. PIN Management, Extension Management, Departments, Phonebook, Budget Alerts, Call Policies, Violations, Billing, and Settings (plus their dialogs) now share the outlined-button/PackIcon look of the dashboards: Refresh buttons with "Last refreshed" indicators, two-state empty panels, consistent dialog headers and owners, and no raw emoji glyphs.
- View preferences are remembered across sessions on the dashboards, Billing, Budget Alerts, Violations, and Settings (filters, selected servers, checkbox states, last-viewed tab, collapsed AI cards).
- User guides refreshed and regenerated for v2.4.0 (all three PDFs).
Version 2.3.5 — 2026-07-02
Stability and correctness release: fixes everything confirmed by the full 4-area code review of 2026-07-02 (data layer, core services, ViewModels, integrations). Schema v44.
Fixed
- Incremental sync could permanently skip CDR records. The sync watermark was the client
PC's wall-clock time (
DateTime.Now), compared against the PBX'scalldateon the next incremental sync. Any PBX whose clock/timezone was behind the client lost the records written after a sync but timestamped below the watermark — forever. The watermark is now the max calldate of the records actually synced (kept in PBX time, since that is what the remote query compares against). - PBX timezone conversion was never stored. The v33
pbx_timezone_idfeature convertedcalldatefor cost/off-peak/classification but bound the INSERT parameter before converting, solocal_cdrkept PBX-local time. The converted (client-local) time is now what gets stored; the raw time is only used for the sync watermark. - Dashboard drill-downs opened the wrong screen. Stale navigation indices from the v2.1.1
sidebar insert: Top Destinations opened Departments instead of Phonebook, Most Expensive
Calls opened the Inbound Dashboard instead of Reports; the Inbound Dashboard's own
drill-downs had the same two bugs. All view indices now come from a shared
NavIndexconstants class. - Every UK mobile call was classified "Premium". The blanket
447/900/1900international premium prefix guesses are removed (447 is the entire UK mobile range); SA premium/value-added prefixes are unchanged. International premium detection belongs to the rate file. - Client billing silently dropped formatted phonebook numbers. The contact lookup was keyed by the raw phonebook number but probed with the normalized dialed number, so contacts stored as e.g. "011 681 5900" never matched and fell off invoices. Keys are now normalized (first entry wins on collisions).
- Deleting a PBX server left orphaned rows. Foreign keys are not enforced on our SQLite
connections, so the schema's
ON DELETE CASCADE/SET NULLnever fired.DeleteAsyncnow cleanssync_status_multi/cdr_schema_multiand re-scopes PIN/extension mappings to "All Servers" in one transaction; a startup repair fixes orphans left by older versions. - Fresh installs were missing the CDR duplicate-prevention index (
idx_local_cdr_unique_call) — the sync path recreated it, but CSV-upload-only installs duplicated every record on re-upload. Migration v44 deduplicates existing rows (keeping the lowest id) and creates the index; the CSV upload path now also ensures it. Fresh installs additionally get thepeeraccount/sequencecolumns (the Inbound Dashboard queries them and crashed on CSV/demo-only installs before a first PBX sync) and the missingidx_local_cdr_billing,idx_local_cdr_linkedidandidx_scheduled_billing_activeindexes. - Regional Windows settings could corrupt stored data. Full
InvariantCulturesweep over all database-facing date formatting/parsing (160+ sites) and money parsing (local_call_premium— "0.15" parsed as 15 on dot-as-thousands locales;VatRatePercentround-trip). Reads of previously stored values keep a current-culture fallback. Same bug family as the v2.3.2 department-CSV fix. - Rate files with a 0-second interval crashed cost calculation (
DivideByZeroExceptionfor any call longer than the first interval). Intervals below 1 are coerced to per-second billing at import and defensively in the cost math. - DB provisioning failures could log plaintext PBX passwords.
SshServicelogged the full command text on error, and provisioning/detection scripts embed the discovered privileged MySQL password. Those calls are now flagged sensitive and log a redacted placeholder. - The 2 long-standing migration-chain test failures. Migration 43's departments
rename-copy assumed columns very old databases lack (
monthly_budget,manager_email,created_at); they are now back-filled defensively before the copy. Migration 28 gained the same interrupted-migration recovery as 43. - Latent calendar bug in a dashboard test (failed during the first 5 days of every month).
Added
- Per-server billing scope. Billing (interactive, detailed invoices and scheduled jobs) now filters calls by the selected PBX server; "All Servers" behaves as before.
- 13 regression tests covering the above (sync watermark/timezone, billing normalization and server filter, classification, v44 dedupe migration, zero-interval rates, server-delete cleanup, fresh-install schema).
Changed
- Invoice/billing amounts round half-up (
MidpointRounding.AwayFromZero) instead of .NET's default banker's rounding. - Call classification pre-normalizes the phonebook once per cache load instead of running a regex over every entry per CDR row — large-phonebook syncs classify significantly faster. The four classify entry points now share one implementation (removes the enum/string drift).
- Removed a dead
pin_mappingsjoin from the main call-records query.
Version 2.3.4 — 2026-06-25
VitalPBX support and setup-wizard improvements.
Added
- VitalPBX CDR credential auto-detect. When a dedicated read-only user cannot be created
(VitalPBX runs MariaDB
rootasunix_socket-only and the SSH login is typically unprivileged, so provisioning fails withERROR 1698), the setup wizard now falls back to detecting and reusing an existing working CDR credential from the PBX's own config. NewIPbxDbUserProvisioningService.DetectWorkingCdrCredentialAsync: it dumps the config over SSH, then in one round-trip tests each discovered credential by locating the schema that holds thecdrtable (information_schema) and verifying aSELECT, and auto-fills the connection with the first that works.DbConfigCredentialParsernow also reads/etc/vitalpbx/database.conf([app]) and parses/etc/odbc.ini(the only source that ties a working user to its CDR database name). On VitalPBX theombutelapp credential is preferred because it can read both the CDR table and the extension directory. - Currency selection in the first-run wizard's Rate File step. The "I have a rate file" upload
path now offers the same currency list as Settings (R / $ / EUR / GBP / A$ / Custom, default
ZAR). Selecting a currency and uploading a rate file applies it app-wide (dashboards,
billing, PDF/Excel exports) via the existing
CurrencyHelper+CurrencyChangedMessageinfrastructure. The built-in South African rates and Skip paths stay ZAR.
Fixed
- VitalPBX extension auto-import (PJSIP). Extensions live in the VitalPBX management database
(
ombutel.ombu_extensions), notasterisk.users/asterisk.sip.ImportExtensionsFromPbxAsyncnow uses a unified fallback chain -users->sip(callerid / id-only) ->ombu_extensions-> generic PJSIPps_endpoints- locating the management database automatically viainformation_schema, so VitalPBX extensions import correctly. - Config-dump parsing dropped a file after one with no trailing newline. VitalPBX
/etc/odbc.inihas no trailing newline, so its last line glued onto the next===FILE:marker and the following file (database.conf) was silently swallowed.SplitSectionsnow splits on the marker token wherever it appears, andBuildDumpCommandemits a newline after each file. - VitalPBX detection check.
SshServiceusedtest -f /etc/vitalpbx, but that path is a directory; corrected totest -d.
Version 2.3.3 — 2026-06-23
Auto-provision a read-only CDR database user over SSH.
Added
- "Create read-only user automatically" in the setup wizard and the Add/Edit PBX Server
dialog. When you have SSH access but not the MySQL password, the app can now create a
dedicated, least-privilege database user (
cdrreporter_ro,SELECTonly) for downloading CDR records - no MySQL root password required. NewPbxDbUserProvisioningService+DbConfigCredentialParser(Core):- Discovers a working privileged DB credential from the PBX's own config over SSH, in
priority order: Issabel
/etc/issabel.confand Elastix/etc/elastix.conf(mysqlrootpwd, full privilege),/root/.my.cnf, FreePBX/etc/freepbx.conf(freepbxuser),/etc/amportal.conf, the liveres_odbc(_additional).confODBC credentials (parsed per[section]),cdr_mysql.conf/res_config_mysql.conf, and finally root via the unix socket (passwordless, as on the FreePBX distro). Active config lines are preferred over;-commented placeholders. - Creates and verifies the user (idempotent:
DROP USER IF EXISTSthen recreate, so re-running rotates/repairs it), grantingSELECTon both the CDR database (asteriskcdrdb) and theasteriskconfig database so PBX extension import keeps working. The user is created for bothlocalhostand127.0.0.1to match the SSH-tunnel path. - Saves the generated credentials into the encrypted server settings automatically.
- Falls back to prompting for a privileged MySQL credential when discovery finds nothing usable or the stored password is outdated (the error message distinguishes the two cases).
- Security: the privileged password is written to a transient
chmod 600defaults file on the PBX (never on themysqlcommand line) and deleted immediately; on any verification failure the half-created user is rolled back.
- Discovers a working privileged DB credential from the PBX's own config over SSH, in
priority order: Issabel
- User guides ship with the app.
QuickStartGuide.pdf,UserGuide.pdfandAdministratorGuide.pdfare now bundled in the installer (next to the executable), exposed as Start Menu shortcuts under the program group, and linked from a new Documentation section in the About dialog (opens each guide in the default PDF viewer). The About dialog also links the Intellivoss YouTube channel (training and overview videos).
Changed
- Installer rebranded to "Q5000". The app now installs to
C:\Program Files\Intellivoss\Q5000(was…\Intellivoss (Pty) Ltd\CDR Reporter) and appears as Q5000 by Intellivoss in Add/Remove Programs, the Start Menu group and shortcuts; "CDR Reporter" is retained as the product's function/description. Existing installs upgrade in place (same upgrade code); user data in%LocalAppData%is unaffected.
Version 2.3.2 — 2026-06-18
Department CSV round-trip fix and full-fidelity export.
Fixed
- Department CSV export no longer corrupts the budget/email on comma-decimal locales.
The export formatted the monthly budget with the current culture, so on a South African
(comma decimal) machine a budget of 1500 was written as
1500,00- an unescaped comma that split the row on import, pushing00into the Manager Email column and dropping the budget cents. Budgets are now written and parsed withInvariantCulture(period decimal) and all fields are RFC-4180 quoted.
Changed
- Department CSV is now a full round-trip format. Export/Import use
Path,Type,CostCenter,MonthlyBudget,ManagerEmail,Status,PinCodes,Extensions, so the department hierarchy (via the>-separated Path), level labels, budgets, and PIN/extension assignments all survive an export -> delete -> import cycle. Parent branches are created before their children. Listed PINs/extensions that don't already exist are created as global (All Servers) mappings; per-server scoping is not carried by this CSV (use the dedicated PIN/Extension CSVs or Backup & Restore for that). The legacy flatName,CostCenter,MonthlyBudget,ManagerEmailformat still imports (auto-detected by header).
Version 2.3.1 — 2026-06-17
Maintenance and housekeeping release; no user-facing feature changes.
Security / Internal
- Committed the email server-side relay and tamper-resistant trial implementation:
email sends via the license-token-authenticated relay (no SMTP credential ships
with the app, so email delivery requires an activated license), and the trial
start date is mirrored to the registry through the new
ITrialClockStore/RegistryTrialClockStoreseam so deleting or editing the local database cannot reset or extend the trial (effective start is the earliest of the database and registry values). - Added unit coverage for the email relay, the tamper-resistant trial,
PhoneNumberNormalizer, andBulkCallerIdentificationService(via anInternalsVisibleTotest seam).
Docs
- Reconciled
TESTING.mdto actualdotnet testcounts (828 Core non-perf + 162 Desktop) and the current v43 schema, and annotated the best-effort temp-file cleanup inComparativeChartDialogso the catches no longer read as swallowed errors.
Version 2.3.0 — 2026-06-15
Adds abandoned-call wait analytics to the Inbound Dashboard.
Added
- Abandoned-call wait analytics on the Inbound Dashboard. Inbound callers who
hang up while waiting in a queue or ring group were previously counted only
inside the overall "missed" total. This release surfaces them as a distinct,
actionable picture:
- Abandonment Rate tile - share of inbound calls where the caller gave up waiting (never answered by a human).
- Median Wait tile - median time callers waited before hanging up, with the 90th-percentile wait shown as detail.
- Wait-Time Breakdown - misdials (under 5 seconds), brief (5-30 seconds), and lost business (over 30 seconds).
- Abandoned Calls by Hour chart - when callers are giving up, useful for spotting under-staffed periods.
- The new metrics are also passed into the Inbound AI Insights summary.
- Wait time is measured as the call-chain span (last hang-up minus first arrival, grouped by linkedid). Sub-5-second hang-ups are treated as misdials and excluded from the abandonment rate.
Notes
- Detection uses the existing CDR data already synced to the local cache; no schema change and no new sync step. Out of scope: on-hold abandonment (those calls were already answered, so they are not detectable from CDR without queue_log).
Version 2.2.0 — 2026-06-13
This release bundles the demo-mode work that accumulated after v2.1.8 (internally tagged v2.1.9-v2.1.12 but never published) together with a full code-review pass addressing 16 ranked findings, a security-hardening change to email, and two internal refactors.
Security
- Email no longer ships an SMTP credential. The old path decrypted
smtp_credentials.db(bundled in the installer) with a hardcoded key, so the shared SMTP account was recoverable from the binary. Email now goes through a server-side relay (https://q5000.co.za/api/email/send) authenticated by the license activation token; no credential lives on the client. Behaviour change: email delivery now requires an activated license — trial installs cannot send. MailKit andsmtp_credentials.dbwere removed from the build. - Trial period is tamper-resistant. The trial start date is mirrored to the
registry (
HKCU\Software\Intellivoss\CDR Reporter); the effective start is the earliest of the database and registry values, so deleting the local database or editing its stored date no longer resets or extends the trial. - SSH tunnel failures surface the real cause (auth / network / declined host key / timeout) instead of a generic message with the detail buried in the log.
Fixed
- Dashboard no longer blocks the UI thread while loading the server list in
demo mode (a synchronous DB read ran inside
Dispatcher.Invoke). - Unhandled non-UI-thread and unobserved-task exceptions are now logged before they can terminate the app silently.
- MySQL connection strings are built with a builder, so SSH/CDR passwords that
contain
;,=or quotes no longer corrupt the connection string. - Inbound/missed-call queries and cost-analysis report name resolution no longer block on sync-over-async calls.
- Guarded a latent crash in the v39 phonebook-category migration on very old databases.
Changed
- Performance: the SA local-call premium is cached when rates load instead of hitting the settings table once per call during sync/reclassify.
- Internal: SA dialling-plan logic consolidated into a single
PhoneNumberNormalizer; the AI "Identify Unknown Callers" flow (previously triplicated across the Dashboard, Inbound and Policy-Violations screens) is now a sharedBulkCallerIdentificationService. - Internal: introduced a dependency-injection composition root and converted
the five main screens (Dashboard, Reports, Billing, Inbound, Settings) to
constructor injection;
DashboardViewModelsplit into partial-class files. - Removed dead code and constants; the test suite builds with zero warnings.
Demo mode (was v2.1.9-v2.1.12)
- Two-branch department hierarchy with pre-baked AI insights, budget warning/ exceeded states, and anchored CDR calls for a realistic walkthrough.
- A display-only "Demo PBX Server" entry so Settings shows a configured server, an "AI Lookup" phonebook category demonstrating the bulk-identify workflow, and startup SSH sync skipped while in demo mode.
- Invoice logo fix and budget-alert UX improvements.
Version 2.1.8 — 2026-06-05
Fixed
- Outbound Dashboard monthly totals were always 0 before first daily sync — SQLite
SUM()returnsNULL(not0) when aggregating over an empty row set. The new outbound-count column added in v2.1.7 (SUM(CASE WHEN ... THEN 1 ELSE 0 END)) was not wrapped inCOALESCE, so on any morning before the first sync of the day (when today's query returns 0 rows),reader.GetInt32threw on theNULLvalue. The exception was silently caught and the method returned a blankDashboardSummary, zeroing all tiles — including "This Month's Calls" and "Total Monthly Calls" which had valid data. Both today's-stats and month-to-date queries now wrap the count column inCOALESCE(..., 0).
Version 2.1.7 — 2026-06-04
Changed
- Outbound Dashboard — outbound-only tiles and charts — All stat tiles (Today's Calls, This Month's Calls, Today's Cost, This Month's Cost) and all charts (Busy Hour, daily trends, top calls, department spending, cost by category) now filter to outbound calls only. "Outbound only" subtitle added to the call-count tiles for clarity.
- Outbound Dashboard — new total-calls tiles — Two new tiles ("Total Calls Today" and "Total Monthly Calls") show combined inbound + outbound counts so both perspectives are visible at once.
- Outbound Dashboard — balanced 4+3 tile layout — Tiles reorganised from a single 5-column row into a 4-tile row (outbound metrics) above a 3-tile row (totals + Violations). Both rows span the full width so right edges align.
- Outbound Dashboard — side-by-side Busy Hour charts — The Daily Trends chart (low value) replaced with a second Busy Hour Analysis bar chart showing all calls (teal). The two charts sit side by side: purple = outbound only (left), teal = all calls (right).
- Busy Hour Analysis subtitle — Both Busy Hour charts now show a muted subtitle ("last 30 days, outbound" and "last 30 days, all calls") so the scope is immediately clear.
- Inbound Dashboard — Busy Hour Analysis replaces heatmap — The Missed Calls Heatmap (Day x Hour) removed; replaced with a purple "Busy Hour Analysis - last 30 days, inbound" bar chart showing all inbound call volume by hour over the past 30 days.
- Inbound Dashboard — Missed Calls by Hour recoloured teal — Consistent with the Outbound Dashboard colour language: purple = directional busy-hour pattern, teal = secondary/mixed metric.
Version 2.1.6 — 2026-06-04
Fixed
- Department names are now unique per-parent only — You can now create a "Sales" department under both "Silverton" and "Jetpark" without an error. Previously the database enforced a global unique constraint on
departments.name, so the same name could only exist once regardless of parent. Schema v43 replaces the column-levelUNIQUEwith two partial indexes: one for top-level departments and one scoped per parent, matching the intended business rule.
Version 2.1.5 — 2026-06-04
Fixed
- Call Policy violations re-evaluated after policy edit — Editing a policy (e.g. changing office hours from 08:00 to 07:30) and clicking "Apply and Refresh" in the Violations view now correctly re-scans all calls in the date window against the updated rules. Previously, any call that had already been flagged was skipped by the deduplication guard, so stale violations stayed visible even after the policy was changed. Unacknowledged violations for each policy are now purged before the re-scan; acknowledged violations are preserved.
Version 2.1.4 — 2026-06-04
Changed
- Department Budget Status grouped by parent — Departments on the Outbound Dashboard are now sorted so all children of the same parent company appear together (e.g. all Jetpark children, then all Silverton children) instead of being interleaved alphabetically. Previously, because parent departments with no budget were filtered out, their children were each treated as independent roots and sorted by raw department name, breaking visual grouping.
- Group separator labels — A colour-coded separator (parent name + thin accent rule) is shown above the first row of each new parent group, making the hierarchy immediately legible.
- Coloured card borders per group — Each department card's border now uses the group's accent colour instead of the flat dark border, reinforcing which rows belong together.
Version 2.1.3 — 2026-06-03
Changed
- User guides refreshed for v2.1.x — Quick Start, User, and Administrator guides updated to cover the AI suite (Insights, Budget Forecast, Security Alert, Suggest Departments), Inbound Dashboard navigation promotion, multi-PBX servers, full PDF/Excel parity (v2.1.0), department name uniqueness scoped per-parent (v2.1.0), schema v42
pending_cost_recalccost recalibration on upgrade (v2.0.7), the Gemini proxy model with cost-optimization (weekly cache, daily cache, anomaly-gated, on-demand), v2.1.1 license token refresh fix, and SMTP credential store design. Newdocs/*-v2.1.3.mdsources; older versions kept as historical references. - PDF generator reads version from
version.json—generate_pdfs.pycover and footer now pull the version live, so they no longer drift when onlyversion.jsonis bumped. CLAUDE.mdsynced to current reality — refreshed version references throughout, updated MAN 3000 Feature Comparison (multi-PBX now done, AI features added), Known Limitations, Future Enhancements, and Current Project Status footer.
Fixed
- Removed leftover debug
Console.WriteLinestatements inUserActivityReportGenerator.cs(two prints of filter date ranges) andReportTemplateRepository.cs(catch-block diagnostic). The repository catch is nowLog.Warningvia Serilog so malformed template JSON is recorded properly instead of being swallowed silently to stdout.
Version 2.1.2 — 2026-06-03
Added
- AI Budget Forecast card (Outbound Dashboard) — A daily-cached Gemini AI card projects each department's month-end spend based on the current daily burn rate. Only visible when at least one department has a monthly budget set and the day of month is >= 3. Highlights at-risk departments (projected to exceed budget) with actionable bullet points. Cache refreshes each morning as burn rates shift.
- AI Security Alert card (Outbound Dashboard) — An orange "Unusual Extension Activity" card that is completely hidden during normal operation. A free SQL check runs every 6 hours comparing each extension's last 7-day call stats against a 30-day baseline. If a volume spike (>3x baseline, >=15 calls) or international call ratio spike (>=20% recent vs <5% baseline, >=5 intl calls) is detected, Gemini AI is called to produce a concise PBX fraud/security alert. AI is only queried when anomalies are actually found, keeping query costs near zero in normal operation.
- AI Suggest Departments for Unassigned Extensions (Extension Management) — A dedicated AI button row below the existing action buttons. Finds extensions with no assigned user name but with recent call activity (>=3 calls in last 30 days), enriches small sets with top destination patterns, and passes them to Gemini AI for department assignment suggestions. Result shown in an info dialog; user assigns manually.
- 8 new unit tests covering
GetExtensionAnomalyDataAsync(volume spike, international spike, no anomaly, empty data) andGetUnassignedExtensionDataAsync(basic, assigned excluded, no unassigned, empty CDR).
Version 2.1.1 — 2026-06-02
Added
- AI Insights on Inbound Dashboard — A weekly-cached Gemini AI summary card now appears below the heatmap, above the Top Missed Extensions grid. Summarises miss rate, recovery rate, avg hold time, top missed extensions, and top callers in 3 actionable bullet points. Same pattern as the Outbound Dashboard: auto-refreshes on data load, 7-day cache, manual Refresh button, hidden for unlicensed tiers.
- Inbound Dashboard promoted to top-level navigation — Inbound Dashboard now appears as the second item in the sidebar (directly after Outbound Dashboard), making it a first-class view rather than a sub-item under Reports.
Changed
- Dashboard renamed to Outbound Dashboard throughout — sidebar label, page title, help panel title, and tooltips updated for clarity now that both dashboards are visible in the nav.
- Inbound Analytics renamed to Inbound Dashboard throughout — sidebar label, page title, help panel title, report type list, and tooltips updated consistently.
Fixed
- License token refresh now uses
NeedsTokenRefresh— Background validation on startup correctly triggers based on token age (5-day interval) rather than the broaderNeedsValidationflag, preventing unnecessary re-validation calls.
Version 2.1.0 — 2026-06-02
Added
- PDF exports now mirror Excel exports across all reports — Every Excel tab is reproduced as its own dedicated page in the PDF. Summary and analysis pages appear first; the raw data table last. Changes per report:
- Call Detail Export — Summary page, Daily Totals page, and Department Totals page (with hierarchy rollup) added to PDF.
- Daily Summary (Detailed mode) — Hourly Distribution page (24-hour table, peak hour highlighted) and Top Expensive Calls page added to PDF.
- Department Report (Detailed mode) — User Breakdown page (all users, 8 columns including Business/Personal %), Daily Trends page, and Call Details page (top 100 by cost) added to PDF.
- Personal vs Business — User Breakdown page (all users with violation highlighting), Flagged Calls page, Department Totals page, and Policy Violations page (red-tinted rows) added to PDF.
- Cost Verification — Zero Cost Calls page (top 100 non-internal zero-cost calls) and Unclassified Destinations page added to PDF.
- Inbound Analytics — Answer Rate and Last Call columns added to the Top Inbound Callers table in the PDF.
Fixed
- Department names no longer require global uniqueness — Creating a department with the same name under a different parent (e.g., "Sales" under Silverton and "Sales" under Jetpark) now succeeds. Uniqueness is enforced per sibling group (same parent) rather than across the entire department tree.
- Extensions in department assignment sorted by extension number — The extension picker in the department dialog now sorts by extension number rather than user name, making it easier to locate systematic extension ranges.
Version 2.0.7 — 2026-06-02
Fixed
- Historic call costs now auto-recompute on first startup after upgrade — v2.0.5/2.0.6 lowered the local call premium but the actual repricing of historic rows in
local_cdronly happened if the user manually clicked "Sync CDR Data" in Settings, because the reclassify-after-sync hook inPerformStartupSyncAsynconly ran when records were actually synced. New schema v42 writes apending_cost_recalc='1'flag (covers installs that already ran v41 on v2.0.5/2.0.6 without setting it). On the next startup,PerformStartupSyncAsyncchecks the flag and runsReclassifyAllCallsAsynconce (which now also recomputes cost per row, per v2.0.6), then clears the flag. This path runs even when auto-sync-on-startup is disabled, so the recalibration takes effect on every install without user intervention. The recalc shows "Recalculating call costs under current rates..." on the splash screen.
Version 2.0.6 — 2026-06-02
Fixed
ReclassifyAllCallsAsyncnow also recomputes stored cost — Hotfix for v2.0.5. The premium recalibration (35% → 15%) only affects calls computed after the change becauselocal_cdr.costis stored at sync/classification time, not derived on read. Historic calls kept their old cost. The reclassify loop now also recomputescostviaCostCalculationService.CalculateCost(dst, billsec, calldate)and writes bothcategoryandcostin a single UPDATE. After upgrading, open Settings and click "Sync CDR Data" once — the reclassify step that runs after sync will re-price the entire back catalogue under the new premium.
Version 2.0.5 — 2026-06-02
Changed
- Local call premium recalibrated from 35% → 15% — The SA local landline premium in
CostCalculationServicewas originally set to 35% to compensate for under-counting from missing SA prefix coverage (02X, 03X, 04X) inIsLocalCall. Commit25bd6d7widened the prefix detector to include all 271–275 area codes, so the over-compensation now inflates totals by ~8% vs. actual Porta billing on a representative customer (10,653 min, R 1,771.56 actual vs. R 1,914.73 calculated). Lowered the default to 15%, which restores the original "~3-4% buffer over actual" design target. Schema v41 force-overwrites thelocal_call_premiumsetting on existing installs. - Server sync progress feedback —
ServerManagementViewModel.SyncSelectedServernow sets anIsSyncingflag, shows an in-progress status message, and disables the Sync/Upload CSV/Clear Cache buttons during the sync so users can no longer double-fire a 30+ second operation. - Dynamic currency labels — "Monthly Budget" in
DepartmentDialogand "Min/Max Cost" inReportsViewnow display the active currency symbol fromCurrencyHelperinstead of hard-coded "(ZAR)" / "(R)".
Fixed
- Inbound SLA % distorted by internal extensions —
CalculateSlaAsyncinInboundAnalyticsService(the queries powering the SLA tile) andGetInboundCallsAsyncinCdrService(the Top Inbound Callers/recent inbound queries) still used the baresrc LIKE '00%'predicate, so 3-digit extensions like009were counted as external 00-prefix international calls. v2.0.4 only patched theInboundWhere()/CallerNum()helpers; the SLA and recent-calls queries did not go through them. SLA queries now reuseInboundWhere()directly andCdrServiceadds the samelength(src) >= 10guard. - Dashboard splash could hang forever if init threw —
DashboardViewModel.InitializeAsynchad no top-level try/catch, so an exception fromGetSettingAsyncor any other init step would leave_startupCompleted = falseand the splash screen would never close. The body is now wrapped in try/finally with the completion flag set in the finally block.
UI / Polish
- Save/Cancel buttons honour Enter/Escape — Added
IsDefault="True"to Save buttons andIsCancel="True"to Cancel/Close buttons across the main data-entry dialogs (PhonebookEntry, PinMapping, ExtensionMapping, Department, Server, EmailSettings, RenameInput) plus a few picker/wizard dialogs. Intentionally skipped on destructive primary actions (e.g. MoveChildrenDialog's "Move & Delete"). - Screenshot and coverage clutter ignored —
.gitignorenow excludesScreenshot*.png,coverage-output/,coverage-report/, and*.coverage. - CHANGELOG entries restored for 2.0.3 / 2.0.4 — Previously only documented through 2.0.2.
- Comment clarifying SSH host-key prompt sync-over-async — Added in
SshService.AttachHostKeyVerificationexplaining that the.GetAwaiter().GetResult()is required by SSH.NET's synchronousHostKeyReceivedevent and is safe because all callersawait.
Version 2.0.4 — 2026-05-29
Fixed
- Internal extensions appearing in Top Inbound Callers list — Extensions starting with
00(e.g.009 - Reception) were matched by the international number filtersrc LIKE '00%'inInboundWhere()andCallerNum(). Added minimum length guard (>= 10) so only real 00-prefix international numbers pass the filter.
Version 2.0.3 — 2026-05-29
Added
- First-run wizard — Rate File Setup step — A new step 3 of 3 between the CDR DB test and the initial data sync. Users can upload a provider rate file or adjust the built-in SA local/mobile rates before the first sync runs, so calls are costed correctly on day one.
Fixed
- Wizard UI freeze during CDR sync — Sync now runs on a
Task.Runbackground thread so the progress bar moves continuously and the app stays responsive. The wizard ViewModel subscribes toProgressChangedwithDispatchermarshalling so updates actually reach the UI. - Sync progress reporting cadence —
CdrSyncServicereports progress every 500 records instead of only after each full 5000-record batch, so the progress bar advances smoothly. - Per-record timezone lookup hot path —
TimeZoneInfo.FindSystemTimeZoneByIdis now cached outside the per-record loop (was being called ~5000× per batch). - Orphaned sync reset targeted the wrong table —
App.xaml.csstartup recovery now resets orphaned syncs fromsync_status_multi(all servers) instead of the legacysync_statustable where onlyserver_id=1was ever cleared.CdrService.GetLastSyncDateAsyncsimilarly switched tosync_status_multi(MAX across all servers) for consistency. - Phonebook false-positive matches on short numbers —
CallClassificationServicepartial-match path now requires a minimum 7-digit length on both sides, preventing short extensions like201from matching unrelated numbers that happen to contain those digits. - Off-peak rate calculation ignored rate-file metadata —
CostCalculationService.IsOffPeakTimenow reads the rate entry'sOffPeakPeriodfield (HH:MM-HH:MM format, handles midnight wrap) before falling back to the hardcoded 08:00-17:00 window.
Version 2.0.2 — 2026-05-29
Fixed
- Phonebook default categories missing on upgraded installs — Three compounding bugs caused Suppliers, Clients, Employees, Creditors, Debtors, General, and Private to be absent from the Category dropdown. (1) Migration v36 only seeded defaults when
phonebook_categorieswas completely empty — databases with existing AI/Client entries never received them. (2) Migration v38 partially fixed this but a multi-row INSERT silently skipped 4 of the 7 categories; migration v39 switched to individual parameterised statements to guarantee each category is inserted independently. (3)DemoDataService.ClearDemoDataAsyncwas deleting Suppliers, Clients, Employees, and Private on every startup via aNOT EXISTScheck that always passed when no phonebook entries used those categories — even on live PBX installs with no demo data. That deletion loop has been removed; categories are user configuration and must survive demo cleanup. Migration v40 re-seeds any categories that were wiped before the fix. The singular AI-generatedClientcategory is also renamed toClientsto match the standard default.
Version 2.0.1 — 2026-05-28
Security
- SSH host key pinning — On first connect to a PBX server, a
HostKeyTrustDialogpresents the server's SHA256 fingerprint for the user to explicitly accept. Subsequent connects compare fingerprints and refuse on mismatch, eliminating the silent MITM risk from SSH.NET's default accept-all behaviour. Trusted hosts are stored in a newtrusted_ssh_hoststable (schema v37). SSH connect timeout extended to 2 minutes only when prompting (unchanged 10s for cached-trust connects). - DPAPI-protected encryption key — Replaced the hardcoded
EncryptionBasePassword+ machine-name salt with a per-install random key protected by Windows DPAPI (DpapiKeyStore). Existing PBX credentials are automatically re-encrypted on first launch viaLegacyCredentialMigrationinside a SQLite transaction. - MSI SHA256 verification — Update flow now verifies the downloaded MSI against a
sha256hash published inrelease.jsonbefore launching the installer.create-release.ps1now emitssha256andfileSizeintorelease.json. Download usesHttpClientwith a progress UI instead of opening the browser. - SQL injection guard in dynamic DDL — Dynamic column and index identifiers in
LocalDatabaseare validated before interpolation into DDL statements. Unsafe identifiers are logged and skipped, preventing SQL injection via a compromised remote PBX MySQL schema. - CSV import size limit — All five CSV importers enforce a 100 MB file-size guard (
AppConstants.CsvImport.MaxBytes) to protect against OOM on malformed or oversized files. - Async dialog constructors —
EmailSettingsDialog,ExtensionMappingDialog,PinMappingDialog, andExtensionImportPreviewDialogswitched from blocking.Wait()/.Resultconstructor calls to asyncLoadedevent handlers.
Added
- Chart embedding in PDF exports — All report types with a chart dialog (Top N/Cost Analysis, Personal vs Business, Inbound/Outbound, Department, Comparative Period) now prompt the user to include the chart when exporting to PDF or ZIP. Charts are rendered at 2x scale with white backgrounds for crisp print output and appended as extra pages.
- 5 new report generator test files —
ComparativePeriodReportGeneratorTests,CostAnalysisReportGeneratorTests,DepartmentReportGeneratorTests,TrendAnalysisReportGeneratorTests,UserActivityReportGeneratorTests. Plus 16 new security tests coveringTrustedSshHostRepositoryround-trips and SQL identifier injection payloads.
Fixed
- Personal vs Business report — 0 personal calls —
CdrServiceCOALESCE order corrected so phonebook entryis_businesstakes priority over the AI category default. The AI category defaultedis_business=1, silently overriding entries explicitly marked as Personal. - PDF chart layout on landscape pages —
ReportGeneratorBasenow usesFitHeightfor landscape pages andFitWidthfor portrait pages. The previousFitWidth-only approach scaled a 1400x1000 chart to 782x558pt on landscape A4 but the content area is only ~468pt tall, causing aQuestPDF DocumentLayoutException. - Top Expensive Calls — CallerID names showing as Unknown —
ResolveUserNameAsyncnow treats non-numericsrcvalues (CallerID display names from the PBX) as the resolved display name directly instead of falling back toUnknown User (Ext: ...). - Top Expensive Calls — chart not appearing in PDF —
CostAnalysisPdfServiceandCostAnalysisReportGeneratorupdated to passChartPngBytesthrough so the chart is included when requested. - Silent failures surfaced — Previously-silent failures in
ExtensionManagementService.DeleteMultipleMappingsAsync,PinManagementService.DeleteMultipleMappingsAsync, andCdrSyncunique-index ensure path are now logged and surfaced to the caller. - Migration v4 index safety — DROP/CREATE of
idx_local_cdr_unique_callis now wrapped in a SQLite transaction so a failed CREATE rolls back the DROP, preventing the table from being left without its uniqueness constraint.
Version 1.9.9 — 2026-05-26
Added
- SSH failure detection at startup — If the PBX SSH connection fails during startup sync (e.g. firewall blocks SSH outside business hours), the app now opens directly to Settings > PBX Servers instead of the Dashboard. A dismissable amber warning banner explains the failure and prompts the user to verify settings and test the connection. Previously the app silently opened a stale Dashboard with no explanation.
- Daily Summary — Inbound/Outbound call disposition — The Call Disposition column in the Daily Summary report is now split into separate Inbound and Outbound columns so managers can see the breakdown at a glance.
- Daily Summary — Configurable Top N filter — Report settings now include a Top N selector (5, 10, 20, 50, All) controlling how many rows appear in the top-callers and top-destinations sections of the Daily Summary.
Changed
- Excel report headers — Q5000 branding — Report headers now display "Q5000" as styled text instead of an embedded logo image, which was overflowing on narrow sheets.
- Professional Excel export formatting — Excel exports have improved column widths, header styling, and layout across all report types.
Fixed
- AI Insights text truncation in Excel — Long AI-generated insight text was being cut off in the Excel AI sheet; now wraps and sizes correctly.
- Unassigned department in inbound analytics — Inbound calls were incorrectly appearing under an "Unassigned" department entry in inbound analytics reports.
- Excel header layout overflow — The report header title was overflowing behind the logo/branding area on narrow sheets.
- AI sheet column widths — Columns in the AI Insights Excel sheet were not auto-sizing correctly.
Version 1.9.3 — 2026-05-19
Fixed
- Build warnings — Resolved all ~80 compiler warnings that appeared on
dotnet run. Fixes include: nullable reference suppressions in 7 report generators (CS8601/02/03/04/8629), removed spuriousasyncon sync-only methods inCdrServiceandDepartmentReportGenerator(CS1998), added[SupportedOSPlatform("windows")]toLicenseServiceclass (CA1416), suppressed transitive NU1701 package-compatibility noise for OpenTK/SkiaSharp deps in Desktop project. No behaviour changes.
Version 1.9.2 — 2026-05-19
Fixed
- Auto-update notification — The background update check now fires 30 seconds after startup instead of 10 minutes. Previously, users who manually checked for updates within the 10-minute window caused the auto-check to be skipped entirely (the manual check saved the timestamp, so the auto-check saw "less than 6 hours since last check" and returned early).
Added
- Skip This Version — The update dialog now includes a "SKIP THIS VERSION" button. Clicking it suppresses auto-update notifications for that specific version until a newer version is available on the server. Manual "Check for Updates" always shows the result regardless of any skipped version.
Version 1.9.1 — 2026-05-19
Added
- N-level department hierarchy — Departments now support unlimited depth parent/child nesting via a
hierarchy_pathmaterialized path column (DB migration v34). Breadcrumb paths display in dropdowns and reports. Subtree rollup (PIN/extension counts, budgets, spending) works correctly at every level. Reports have a new "Include Sub-levels" checkbox. BillingService uses subtree queries instead of single-level child lookup. - LiveCharts2 charts — Dashboard Busy Hour replaced with LiveCharts2 vertical
ColumnSeries(deep purple, inline data labels). Dashboard Daily Trends replaced with smooth filledLineSeries(dual Y-axis, purple = calls, orange = cost). Inbound Analytics Missed Calls by Hour replaced with LiveCharts2 vertical bars. - Inbound Analytics — Missed Calls Heatmap (Day x Hour) — Purple-gradient heatmap plotting missed call volume by day-of-week vs. hour-of-day. Reveals which time slots have the highest missed call concentration. SQL uses
COUNT(DISTINCT uniqueid)and external-caller filter to match summary tile counts exactly. - Inbound Analytics — Recovery Rate tile — Shows how many unique missed callers called back and got answered in the period (e.g. "3 of 18 callers called back"). Gives managers a quick callback effectiveness metric.
- Inbound Analytics — SLA tile — Percentage of answered inbound calls where ring time (duration - billsec) was 15 seconds or less. Displayed in green at 100%.
- Inbound Analytics layout — Charts (heatmap + hourly bar) now appear directly below the 5 summary tiles; data grids (Top Missed Extensions, Top Inbound Callers) appear below the charts.
- Department deletion — Move Children dialog — Deleting a parent department that still has children now opens
MoveChildrenDialoginstead of blocking with an error. User picks a new parent (or "Make root departments") and the deletion proceeds safely. - PrefixTrie data structure — New
PrefixTrie<T>for O(L) longest-prefix phone number matching. Used internally for rate lookups; faster than linear scan on large rate files. - MissedDestinationSummary model — New model for missed destination analytics with phonebook-resolved friendly name display.
- Release script dirty-tree guard —
create-release.ps1now refuses to build if any tracked file has uncommitted changes, ensuring the MSI always matches git history. - Expanded test suite — 14 new N-level hierarchy tests added to DepartmentRepositoryTests. 10 new service/viewmodel test files (AiServiceTests, DepartmentServiceTests, ExtensionManagementServiceTests, PhonebookServiceTests, PinManagementServiceTests, PrefixTrieTests, SettingsServiceTests, BillingViewModelTests, BudgetAlertsViewModelTests, ReportsViewModelTests). Total: 842 tests (725 Core + 117 Desktop), 0 failures.
Version 1.9.0 — 2026-05-14
Added
- Gemini AI Service — New AI infrastructure proxied through q5000.co.za/api/ai; no API key required by users. License-tier daily limits: Starter 50/day, Business 100/day, Professional/Enterprise unlimited. All lookups pass through PhoneLookupGuard which skips internal extensions (<=5 digits), SA emergency/service numbers, SA mobile numbers (06x-085x), numbers already in phonebook, and star/hash feature codes to avoid unnecessary API calls.
- Dashboard AI Insights card — Automatically appears when the license has AI enabled. Displays a weekly-cached AI-generated analysis of call patterns and trends. Refreshes automatically after 7 days; a manual Refresh button is always available. Insight is generated from current dashboard summary data.
- PBX Timezone configuration — Server configuration dialog now includes a PBX Timezone field (Windows timezone ID, e.g. "South Africa Standard Time"). CDR sync automatically converts calldate from the PBX timezone to the local PC timezone. Leave blank when the PBX and PC share the same timezone.
- PIN Import — Server Assignment dialog — Importing a PIN CSV now shows an options dialog to assign imported PINs to a specific server (or "All Servers") and whether to skip duplicates. Replaces the previous silent global assignment.
- Expanded test suite — 14 new test files added: BillingRepository, CallPolicyRepository, InvoiceRepository, ScheduledBillingJobRepository, ServerRepository, AccountingExportService, BillingService, BudgetAlertService, CallPolicyService, CdrUploadService, DashboardStatisticsService, InboundAnalyticsService, PhoneLookupGuard, and RateFileService. Total tests raised from 239 to 667 with 0 failures.
Fixed
- CdrService — server-scoped extension lookups — Extension mapping joins now use correlated subqueries that scope the lookup to the call's server_id first, falling back to global (server_id IS NULL) extensions. Prevents wrong user names appearing when the same extension number is configured on multiple servers.
- Reports — OriginalSrc missing from CDR records — All CdrService queries now include
c.src as original_srcso report generators access the raw extension number before phonebook/PIN name resolution. Fixes grouping edge cases in reports where the resolved display name was incorrectly used as the extension identifier. - Cost Verification report — SA 087x calls showing as unrated — Database migration adds a catch-all rate entry for 087x numbers (South Africa NonGeo VoIP, 0.3456/min) that was missing from the default rate file. The 0876544800-style numbers no longer show unrated in Cost Verification.
- CDR query performance — Schema migration adds
idx_local_cdr_calldate_srcandidx_local_cdr_calldate_dstcomposite indexes, speeding up date-range queries that filter or group by extension number.
Version 1.8.9 — 2026-05-13
Added
- User Activity Report — Incoming/Outgoing columns — The User Activity report now shows separate
IncomingandOutgoingcall counts per user alongside the existingTotal Callscolumn, giving a clear inbound/outbound breakdown in both the preview and the generated PDF/Excel. - Inbound Analytics Report — enhanced preview — The report preview now shows summary tiles (Total Inbound, Total Missed, Miss Rate) and a Top Inbound Callers section, so the preview matches what the full PDF/Excel report contains.
Fixed
- All reports — 0 inbound calls for ring group extensions (root cause fix) — When inbound calls arrive via a ring group (e.g., ring group 600), the PBX creates CDR records with
dst=600(the ring group number). The previous code only looked atsrcto identify the calling extension, so all inbound calls were silently skipped. Two-stage fix applied across all affected reports: (1) checkOriginalDstfor direct-to-extension calls, (2) parseDstChannel(e.g.,SIP/206-00008671→206) for ring group answered calls. Only ANSWERED ring group legs are attributed to avoid counting every ringing extension for missed calls. - Reports > Summary — 0 inbound calls —
CalculateUserExtensionBreakdown()now correctly attributes ring group ANSWERED calls to the answering extension. Previously every inbound call was skipped because the external caller number insrcnever matched any extension. - Reports > Department — inbound calls attributed to UNKNOWN user — The department filter now includes ring group ANSWERED calls via
OriginalDstandDstChannelparsing.CalculateUserBreakdown()uses an explicitisInboundflag so direction counting is always correct regardless of whether the ring group name has been phonebook-resolved. - Reports > User Activity — all inbound calls excluded, Unknown User for all extensions — Rewrote the user aggregation using a new
BuildUserSummaries()helper that handles both outgoing (attributed viasrc) and inbound (attributed viaOriginalDst/DstChannel) in a single pass. Fixed a secondary bug where CdrService SQL COALESCE resolvessrcto the user name (e.g.,"Pieter Labuschagne"instead of"206"); a reverse-lookup against the extension cache now correctly maps the resolved name back to the extension number before grouping. - Reports > Inbound vs Outbound — ring group calls double-counted — Applied
UniqueIddeduplication before counting (keeping the ANSWERED record when multiple CDR rows share the sameUniqueId). Direction classification switched fromDst.Length-based heuristic to source-based: calls with an externalsrc(≥10 digits or starts with+/00) are inbound; calls with an externalDstare outbound. Internal extension-to-extension calls now correctly classified as Unknown rather than Inbound. - Reports > Inbound Analytics — inflated missed counts and false ring group entries — All three SQL queries in
InboundAnalyticsServicenow useCOUNT(DISTINCT uniqueid)instead ofCOUNT(*)to count calls rather than CDR rows. The missed extension query was rewritten as a two-CTE query: the first CTE filters to inbound missed calls (excluding anyuniqueidthat has an ANSWERED record); the second extracts the real extension fromDstChannel(SIP/206-XXXXXXXX→206) so ring group members are correctly identified. A finalINNER JOINonextension_mappingsensures ring group numbers (e.g.,600) never appear as missed extensions.
Version 1.8.8 — 2026-05-12
Added
- AI Pattern Detection — Personal vs Business report — New "Detect & Save Personal Numbers" button in the Personal vs Business report scans CDR records for unclassified SA mobile numbers (06x, 07x, 081-085) called in suspicious patterns: 3+ times in a single day, on 5+ distinct days in the period, or average call duration over 10 minutes. Detected numbers are shown in a confirmation dialog and saved to the phonebook as Personal (Category: AI). Re-running the report then classifies those calls correctly. VoIP ranges (086-089) are excluded from detection.
- Extension-Only PBX support — auto-detect mode — After each CDR sync the app now detects whether the PBX uses PINs, extensions, or a mix of both and stores the result automatically. Settings > Call Identifier Mode shows the detected mode and lets administrators override it manually (Auto / Extension Only / PIN Only / Mixed). This detection drives correct user grouping across all reports without any manual configuration.
- Startup orphaned reference repair — On every startup,
department_idvalues inextension_mappingsandpin_mappingsthat point to deleted departments are automatically nulled out. Silently repairs existing data integrity issues with no user action required.
Fixed
- Summary Report — extension-only PBX showing only 1 extension — On a PBX that uses extensions instead of PINs, the Summary Report was showing only the one extension that had a named user in the database. Root cause: the SQL
COALESCE(user_name, ..., src)returned an empty string""(not NULL) whenuser_namewas blank, causing all C# fallback logic to silently skip those calls. Fixed by addingc.src as original_srcto the CDR query and a newOriginalSrcproperty onCallRecord. All user breakdown logic now uses the raw extension number fromOriginalSrcrather than the resolved display value. - Summary Report PDF — Extension/User Breakdown missing — The Summary view mode PDF never rendered the Extension/User Breakdown table; it was only computed but never written to the document. Fixed; breakdown now appears in both Summary and Detailed PDF exports.
- Summary Report PDF — Detailed view missing per-extension call records — The Detailed view mode showed only "Top 10 Expensive Calls", with no breakdown by user/extension. Detailed PDF now includes the user/extension breakdown summary table followed by individual call records per extension (Date/Time, Destination, Direction, Duration, Cost).
- Personal Business Report — extension-only calls excluded — The report grouped calls by PIN (
AccountCode) only; any call from a PBX that uses extensions without PINs was silently omitted. The report now groups by PIN when available, falling back to extension number (short numeric, ≤6 digits) so all calls appear regardless of whether PINs are configured. User name resolution tries PIN lookup first, then extension mapping, then displays the extension number. - Department disappearing after restart (critical) —
DemoDataService.ClearDemoDataAsync()deleted departments by name only ('Sales','Finance','Operations', etc.). A user-created department named "Sales" would be silently wiped on the next startup sync. Fixed by addingAND manager_email LIKE '%@demo.local'— demo data always uses that email domain, so real user departments with the same names are never touched. - Extension dropdown showing "[Available]" on in-use extensions (orange) — Extensions with a stale
department_idpointing to a deleted department showed orange coloring (correctly flagged as "in use") but displayed the text "[Available]" (incorrect — the department was gone). TheDisplayTextproperty now shows[In Use]in this case instead of the misleading[Available]. - Phonebook delete crash (NullReferenceException at line 282) — After deleting an entry,
LoadPhonebookEntriesAsync()reloaded the list which clearedSelectedEntry. A subsequentLog.Informationcall then crashed on the now-null reference. The deleted entry's name is now captured before reloading. - Phonebook going blank after delete or any reload —
Categories.Clear()during a reload caused WPF to reset the bound ComboBox selection to null, which triggeredApplyFilters()with a null category that matched no entries. The selected category is now preserved across the rebuild and restored afterwards (falling back to "All Categories" if the previously selected category was removed). ClearDemoDataAsyncleaving orphaned department references — When demo departments were deleted with FK checks disabled,extension_mappingsandpin_mappingsrows retained theirdepartment_idvalues pointing to the now-deleted departments.ClearDemoDataAsyncnow nulls out those references before deleting the departments, preventing the orphan build-up that caused the "[Available]" display bug above.
Version 1.8.7 — 2026-05-12
Added
- AI Insights in PDF exports — When an AI insight has been generated before exporting, a styled "AI Insights" section is appended at the bottom of the PDF. Each report type uses a contextual title: "AI Executive Summary" (Summary Report), "AI Audit Findings" (Personal vs Business), "AI Forecast & Predictions" (Trend Analysis).
- AI Insights in Excel exports — An "AI Insights" sheet is added as the last tab in the workbook when an AI insight was generated before exporting. Includes generation timestamp and full AI response text.
Fixed
- AI insight buttons not appearing in Reports view — The "AI Summary", "AI Forecast", and "AI Audit" buttons were never shown despite the license having AI enabled.
AiService.DailyLimitdefaults to0untilRefreshDailyLimitAsync()is called — this call was missing inReportsViewModel, soIsAvailablealways returnedfalse. Fixed by callingRefreshDailyLimitAsync()inPreviewReportAsync()before checking availability.
Version 1.8.6 — 2026-05-07
Fixed
- Sync CDR Data button — Manual sync was showing "No server configured" despite a server being configured. The button was reading from the legacy
servertable instead of the currentserverstable. Now correctly resolves the selected server from the dropdown viaServerRepository. - Department Budget Status — R0.00 spend — Department spending was always zero when departments used extension assignments instead of PIN codes. The spending query now joins both
pin_mappingsandextension_mappings, so costs are correctly attributed regardless of which mapping type is used. - DemoDataService startup warning — FK constraint error ("SQLite Error 19") logged on every startup during demo data cleanup. Root cause: deleting a parent department row before its child rows. Fixed by disabling FK checks around the cleanup transaction (SQLite requires this pragma outside an active transaction) and re-enabling them in a
finallyblock. - Missing transactions on multi-step DB operations —
DepartmentRepository.DeleteAsync(3-step unassign + delete) andServerRepository.SetPrimaryServerAsync(clear-then-set) were not wrapped in transactions, risking partial writes. Both now use atomic transactions. - Silent cost calculation failures during sync — Cost calculation errors during batch insert were silently swallowed. Failures are now counted and surfaced as a warning in the sync status message.
- Concurrent sync race condition —
IsSyncingAsync+SetSyncStatusAsync(true)were two separate operations, allowing two sync jobs to start simultaneously. Replaced with a single atomicTryStartSyncAsyncusingUPDATE WHERE is_syncing = 0(rows affected = 0 means already locked). - Decimal precision loss in billing — Floating-point cast
(decimal)(double)valueinCdrService.GetValue<T>could silently lose precision on billing amounts. Replaced withConvert.ToDecimal(). - Integer overflow in duration sums —
MissedCallSummary.TotalRingSecondswasint, overflowing on large datasets with many missed calls. Changed tolong; correspondingreader.GetInt64()call updated inInboundAnalyticsService. - Missing database indexes — Schema v31 adds 6 composite indexes:
call_policy_violations(server_id, detected_at),billing_runs(created_at),pin_mappings(department_id),pin_mappings(server_id, is_active),extension_mappings(department_id),departments(parent_id). Applied automatically on first launch. - UI thread blocking in ViewModel constructors —
DashboardViewModel,ReportsViewModel, andFirstRunSetupWizardViewModelwere calling_database.InitializeAsync().GetAwaiter().GetResult()in constructors, blocking the UI thread. Database init is now awaited in the first async method instead. - Memory leaks from event subscriptions —
ReportsViewModelandSettingsViewModelwere not unsubscribing fromCdrSyncServiceevents orWeakReferenceMessengeron disposal. Both now implementIDisposablewith proper cleanup. - ObservableCollection background thread modification —
ExtensionImportPreviewViewModel.LoadCsvPreviewAsyncwas buildingPreviewItemson a background thread. Items are now collected in aList<T>on the background thread and marshalled to the UI thread viaDispatcher.Invoke. - Thread-unsafe event invocation in CdrSyncService —
ProgressChangedandSyncCompletedevent handlers were not captured before invocation, allowing a race where the handler could be unsubscribed between the null-check and the invoke. Both now capture the handler reference first. - Update check crash on app shutdown — Update check dialog could throw
NullReferenceExceptionif the main window was already closed/unloaded. Added null andIsLoadedguard before showing the dialog. - Race condition polling for data readiness —
ReportsViewModelused polling loops (while (!_pinsLoaded) await Task.Delay(100)) to wait for async data. Replaced withTaskCompletionSource<bool>signals andTask.WhenAnywith a 5-second timeout. - Partial import success not reported — Cost calculation failures during CDR batch insert were not counted or reported. Failure counts are now tracked and included in the sync completion message.
- CSV import robustness — PIN and extension CSV import now uses RFC 4180-compliant quoted field parsing. Fields containing commas (e.g.
"Smith, John") are correctly handled instead of being split mid-name.
Version 1.8.5 — 2026-05-07
Added
- Top Inbound Callers — Bulk Identify Unknown — New bulk action button (purple, right of section heading) triggers AI lookup on all unknown callers at once, matching the Dashboard style.
- Top Inbound Callers — per-row AI Lookup + Google Search — Each row in the Top Callers grid now has an AI Lookup (brain icon) and Google Search (magnify icon) button in the last column. Uses CallerNumber as the lookup parameter, identical behaviour to Dashboard > Top Reports. Icon legend shown inline in the heading.
- Schema v30 — calldate index —
idx_local_cdr_calldateindex added onlocal_cdr(calldate)for faster date-range queries during sync and reclassification. Applied automatically on first launch.
Changed
- Targeted reclassification after sync —
ReclassifyCallsSinceAsync(lastSyncDate)is called after a sync that adds new records, limiting reclassification to only the newly synced window. Falls back toReclassifyAllCallsAsync()on a first-ever sync. Skips reclassification entirely when zero records were synced (most common case on quick reopen).
Fixed
- Top Missed Extensions —
GetTopMissedExtensionsAsync()now filtersAND length(c.dst) < 9, excluding external numbers (10 digits) and 9-digit near-miss misdials. Only real internal extensions (<=8 chars) are shown.
Version 1.8.4 — 2026-05-06
Added
- Multi-PBX Extension Assignments — Extensions can now be linked to a specific server. When multiple PBX systems share the same extension numbers (e.g. ext 200 = Reception on each server), each is mapped independently. CDR lookups resolve the correct name based on which server the call came from. Global (All Servers) extensions act as a fallback.
- Server column in Extension Management — Extension list now shows which server each extension belongs to, or "All Servers" for global entries.
- Extension import assigns to server — Import preview dialog now has "Assign all to server" instead of "Assign all to department". Duplicate detection is per-server, so the same extension number can be imported for multiple servers without false duplicate warnings.
- Department dialog Server filter — A single server selector now sits above both PIN Codes and Extension Numbers sections. Selecting a server filters both lists to show only PINs and Extensions belonging to that server (plus global entries). Selection state is preserved when switching servers.
Fixed
- Cost Verification report — Toll-free numbers (rate found but R0.00) are now detected as a separate category. Headline stat shows "Truly Missing Rate %" rather than total zero-cost count. Percentage breakdown includes absolute counts.
- Child department editing — Fixed issue where child departments could not be edited correctly.
- Factory reset — Added factory reset button in Settings.
- First-run sync — First-run PBX setup wizard now performs the CDR sync during setup instead of deferring it to after the dashboard loads.
Version 1.8.3 — 2026-05-06
Fixed
- Restore & Launch: After restoring from backup in the setup wizard, clicking "Launch App" now shows the splash screen and performs the SSH sync before opening the dashboard. Previously it skipped straight to the dashboard, causing slowness as records uploaded in the background.
Version 1.8.1 — 2026-05-04
Added
- Brain (AI) button in Policy Violations — each violation row now has an AI lookup button alongside the existing Google search, Acknowledge, and Delete actions.
- Actions column header — all 4 dashboard call grids (Most Expensive, Longest Duration, Top Destinations, Recent Calls) now show an "Actions" header above the Brain and Magnify buttons.
- Server-side Gemini prompt system —
GEMINI.mddomain knowledge file on the proxy with SA area code reference, number format conversion rules, category taxonomy, and forced Google Search grounding for phone lookups.
Fixed
- Brain button passed phonebook name instead of raw number — Dashboard Most Expensive and Longest Duration grids were binding
CommandParameterto the COALESCE-resolved display value (Dst). Changed toOriginalDstso the raw dialled number is always sent to the AI. - Dashboard top calls missing
original_dst— SQL query for top expensive and longest calls did not selectc.dst as original_dst, causingOriginalDstto be null and the Brain button to silently do nothing. Fixed by adding the column to both queries and mapping it inMapCallRecord. - AI lookup silently skipped for numbers already in phonebook —
PhoneLookupService.LookupAsyncwas blocking user-initiated lookups when the number existed in the phonebook (a guard intended for bulk auto-lookups only). User-initiated Brain button calls now passforceIfAlreadyExists: trueto bypass this check. - AI phone lookup returning Unknown for known SA businesses — prompt updated to explicitly instruct Gemini to search both local format (
0218311039) and international format (+27 21 831 1039). Server-side now forces search grounding regardless of client flag and auto-converts number formats.
Version 1.8.0 — 2026-05-02
Added
- Gemini AI Integration — AI-powered phone number lookup and dashboard intelligence, included with all monthly subscription tiers (Starter: 50/day, Business: 100/day, Professional: unlimited). Once-off license holders see AI features but cannot use them.
- AI Phone Lookup (Phonebook) — "AI Look Up Number" button: enter any SA number and the AI identifies the business, pre-filling the Add Entry dialog with name and suggested category. Numbers already in the phonebook, internal extensions (<=5 digits), emergency numbers (112, 10111, etc.) and Telkom service codes are automatically skipped.
- Bulk AI Lookup — "Look Up All Unknown" button: identifies top unknown destinations from recent CDR records in batches, shows a summary for confirmation, then adds selected entries to the phonebook.
- Right-Click AI Lookup on Expensive Calls — right-click any call in the Most Expensive Calls list on the dashboard to "AI Look up & add to phonebook" or fall back to "Search on Google". Google search always works regardless of AI access or daily limit.
- Dashboard AI Insights Card — automatically generates a 2-3 bullet management summary of current month call statistics on dashboard load (uses Gemini
dashboardInsightsprompt, cached for the session, refreshable). - Dashboard Anomaly Detection — after 7+ days of data in the current month, a second AI call compares current vs previous month stats and appends flagged anomalies to the Insights card.
- AI Features section in Settings — shows AI status (Enabled/Not available), daily limit from license tier, and used-today count. No API key required — access is fully license-driven.
- License-tier AI gating — AI daily limits (
ai_enabled,ai_daily_limit) added to license validation response; app enforces limits with a "Daily limit reached — resets at midnight" snackbar. - PhoneLookupGuard — static skip list prevents unnecessary API calls for internal extensions,
*/#feature codes, emergency numbers, SA service codes, and numbers already in the phonebook. - Gemini prompt templates (
gemini-prompts.json) — embedded resource with SA-specific guardrails: local 0XX format only, no carrier names, no residential names, honest unknowns, max 50-char names. - Policy Violations pagination — violations list now loads in pages of 50 with Previous/Next navigation and a total count display, replacing the previous hard limit of 500 rows.
- Policy Violations phone search — right-click or action button to search a violation's destination number on Google directly from the violations view.
- Policy Violations — Add to Phonebook — acknowledging a violation now offers to navigate to the Phonebook view with the dialled number pre-filled for quick entry.
- PhoneSearchHelper — new shared helper (
Helpers/PhoneSearchHelper.cs) used by Dashboard and Policy Violations for consistent Google phone search behaviour.
Changed
- Cost calculation decimal precision — rate file prices now loaded with
Convert.ToDecimal(reader.GetValue())instead of(decimal)reader.GetDouble(), preventing floating-point rounding errors on cost calculations. - Date parsing culture-safe —
DateTime.Parsecalls inCostCalculationServicenow useCultureInfo.InvariantCultureto avoid locale-specific parse failures. - Rate cache thread safety — double-checked locking added to
CostCalculationService._rateCacheinitialisation, preventing race conditions on first load.
Fixed
- Backup SQL injection —
BackupRestoreService.GetTableInfo()now validates table names against an allowlist before interpolating into SQL, fixing an injection vector in the backup/restore path. - Violations count query — added
GetViolationsCountAsync()toCallPolicyRepositoryso the violations view can show accurate totals without loading all rows. DestinationSummary.SearchableNumber— added computed property returningRawPhoneNumber ?? Destinationfor consistent phone search across dashboard and phonebook navigation.
Version 1.7.9 — 2026-05-01
Fixed
- Deadlock Prevention: Eliminated potential UI freezes in
LicenseService(converted.Result/.Wait()to proper async/await) andCallClassificationService(added synchronous cache load to avoid.Wait()on async methods) - SSH Resource Leak: SSH client is now properly disposed if
Connect()throws, preventing leaked connections on failed SSH attempts - SQL Injection in CSV Upload: Column names from CSV headers are now sanitized (alphanumeric and underscore only) before use in SQL statements, preventing injection via crafted CSV files
- Unobserved Task Exception: Fire-and-forget
Task.Runin report detail view is now observed with discard assignment, preventing potential app crashes from unhandled exceptions - Substring Bounds Check: Added
Length > 1guard beforeSubstring(1)in billing phone number normalization to prevent edge case with single-character strings - Debug Output Cleanup: Replaced
Console.WriteLinedebug statements inCdrServicewith proper SerilogLog.Debugcalls; removed leftover account code debugging output - Clean Shutdown: Added
CancellationTokenSourcefor background startup tasks (update check delay), cancelled on app exit. Removed blockingThread.Sleep(500)during shutdown - Test Fix: Fixed
GetCategories_Should_Return_All_Unique_Categoriestest to populatephonebook_categoriestable (test was stale after schema migration to dedicated categories table) - Test Cleanup: Removed 3 empty
UnitTest1.csplaceholder files from Core, Desktop, and Integration test projects
Version 1.7.6 — 2026-04-28
Fixed
- Startup Sync During Splash Screen: CDR sync now runs during the splash screen again (before the main window opens), showing "Connecting to [server] via SSH..." and "Synced N CDR records..." progress messages. The main window only opens after sync completes, so the dashboard loads with up-to-date data and is not sluggish. Previously this sync had been moved into
DashboardViewModeland ran after the window was already visible.
Version 1.7.5 — 2026-04-28
Fixed
- Sync After Restore: Syncing from the dashboard now works immediately after restoring a backup — no need to manually go to Settings and test the SSH/CDR connections first. The
cdr_schema_multitable (which stores the detected CDR column layout per server) is now exported tocdr_schema.jsonand restored alongside server credentials, with server ID translation handled the same way as department IDs. Old backups withoutcdr_schema.jsonsilently skip this step (no regression).
Version 1.7.4 — 2026-04-28
Fixed
- PIN and Extension Department Links on Restore: PIN mappings and extension mappings now correctly link to their departments after a restore. Previously the backed-up
department_idvalues were written as-is, but auto-increment assigns new IDs after restore, so all assignments pointed to the wrong departments or null. - Department Hierarchy Restore: Parent/child department relationships are now correctly restored. The backup export was missing
parent_identirely. The restore now uses a two-pass approach — departments are inserted first, then parent links are wired by name so backed-up IDs are never used directly. - Both fixes share the same underlying solution: a
backupId → newIdmap built after departments are restored, used to translate alldepartment_idreferences in pins and extensions.
Version 1.7.3 — 2026-04-28 (superseded by 1.7.4)
Fixed
- Department hierarchy restore (partial fix — PIN/extension links still broken).
Version 1.7.2 — 2026-04-28
Added
- First Run Wizard on Reinstall: The setup wizard now launches automatically on the first app start after any new MSI install, even when local data (database, server config) already exists from a previous install. The MSI writes a
NewInstallregistry flag that the app reads once and clears — subsequent launches go straight to the main window. - Restore from Backup on Data Step: The setup wizard's Data step (Step 3) now includes a "Restore from Backup" option alongside Sync/CSV/Demo/Skip. Previously only accessible from the Welcome screen — now reachable from the SSH and CSV setup paths too.
Removed
- Uninstaller data-deletion prompt: Dropped — Windows Programs & Features launches the MSI uninstaller in basic UI mode, bypassing the MSI UI sequence entirely.
Version 1.7.1 — 2026-04-27 (do not use — data loss bug)
Added
- Uninstaller data cleanup dialog (WiX dialog injection approach — had a bug that caused silent deletion).
Version 1.7.0 — 2026-04-26
This is a major stability and onboarding release. It consolidates all improvements made across v1.6.5 through v1.6.30 — primarily focused on making fresh installs reliable, introducing a guided setup wizard, adding demo mode, and hardening security and backup/restore.
Added
- First Run Setup Wizard: A guided setup wizard now appears automatically on fresh installs. Presents three options upfront: Connect via SSH, Upload a CDR CSV file, or Try Demo Mode. Walks through SSH connection, CDR database configuration, and initial data download before the main window opens
- Demo Mode: Load a full realistic dataset in one click — 300 calls over 30 business days, 6 departments with budgets, 12 staff with PINs and extensions, 19 phonebook contacts across 5 colour-coded categories, pre-triggered budget alerts, and 2 call policies with violations. Demo data is automatically cleared on first real PBX sync
- Clear Demo Data: New button in Settings > PBX Servers (visible only in demo mode). Wizard also prompts to clear demo data when switching to SSH or CSV import while demo is active
- Restore from Backup in Wizard: Restore from a backup file directly from the wizard welcome screen — all data, settings, and PBX configuration restored in one step. Auto-backups are detected automatically (no password prompt required)
- Zero-Config Email: Budget alerts and billing reports are now sent automatically via the company mail server with no SMTP setup required. Credentials are shipped pre-encrypted in an AES-256-GCM credentials file (
smtp_credentials.db) — the password never appears in plaintext anywhere - Startup Schema Verification: A startup check automatically detects and repairs missing database tables and columns, preventing crashes caused by schema drift between upgrades
Changed
- Setup Wizard Theme: Wizard is restyled to match the app's dark navy + cyan Material Design theme — cards, headers, footers, and feedback boxes are all consistent with the main application
- Demo Data Dialogs: All demo mode prompts (in wizard and Settings) now use styled app dialogs instead of plain Windows message boxes
- EULA Branding: Installer license agreement updated to Intellivoss (Pty) Ltd, correct version number displayed at install time, governing law set to Republic of South Africa
- Email Sender: Outgoing emails sent from
Q5000@intellivoss.comvia the company mail server
Removed
- Email Settings Button: Removed from Budget Alerts view — SMTP is now centrally managed via the pre-configured credentials file
Fixed
- Fresh Install Crashes: Complete database schema now created on fresh installs — all missing columns and tables (including
dcontext,fixed_monthly_fee,invoice_settings,invoice_sequence,scheduled_billing_jobs, all invoice/multi-server billing columns) are present from the start - CSV Import: Supports Asterisk/FreePBX, xDR, and VoIPmonitor formats with auto delimiter detection. Fixed BOM stripping bug that corrupted the first column header. Import is now 10–100x faster using transactional batch inserts. Progress updates every 1,000 records instead of freezing
- CSV Import Server: CSV-only installs now create a proper server entry so imported records appear on the dashboard immediately (no more orphaned
server_id=0records) - Backup Restore: Restore no longer fails with FOREIGN KEY constraint errors. Server/PBX settings now correctly restored from backup (reads from
serverstable). SSH and DB password fields pre-populate correctly after restore. License status reflects correctly after restore — no restart needed - Reports in Demo Mode: All report generators now query across all servers — demo CDR records (server_id=0) were previously excluded by a hardcoded server_id=1 filter. Affects all report types
- Call Policies After Demo Load: Fixed enum mismatch (
DurationLimit) that caused an error when loading call policies after seeding demo data - Startup Race Condition: Fixed race condition where the main window stayed hidden after the splash screen closed
- Unhandled Exceptions: App no longer closes silently on unexpected errors — exceptions are now logged and shown in a user-friendly dialog
Security
- SQL Injection Prevention: CDR sync now validates all dynamic column names against a strict whitelist and uses parameterized queries for date values
- Auto-Backup Encryption: Backup encryption password is now derived from machine identity (machine name + username via SHA-256) instead of a hardcoded value. Auto-backups restore seamlessly without a password prompt
- Dependency Updates: MailKit updated to 4.16.0 (resolves known moderate severity vulnerability); Microsoft.Data.Sqlite updated to 10.0.5
Version 1.6.7 — 2026-04-24
Added
- First Run Setup Wizard: New guided setup wizard appears automatically on fresh installs (when no PBX server is configured). Walks the user through SSH connection, CDR database, and initial data download before the main window opens.
- CSV Import in Wizard: Users without SSH access can upload a CDR CSV file directly in the wizard to populate the database on first run.
- Unhandled Exception Handler:
DispatcherUnhandledExceptionnow catches silent WPF crashes, logs them to the log file, and shows a user-friendly error dialog instead of silently closing the app.
Fixed
- Fresh Install Crash: App no longer crashes on first launch. The wizard ensures CDR schema columns are populated before the dashboard loads, fixing the
local_cdrincomplete schema issue. - Splash Screen Hang: Main window now appears immediately after dashboard data loads. Background CDR sync no longer blocks the splash screen from revealing the main window.
Version 1.6.6 — 2026-04-24
Added
- Automated Email Delivery: SMTP email now works out of the box on every installation with no user setup required. Budget alerts and billing reports are sent automatically via the company mail server.
- SmtpCredentialStore: New encrypted credential store (
smtp_credentials.db) ships with the installer. All SMTP settings are pre-encrypted using AES-256-GCM with a fixed app-level key — the password never appears in plaintext anywhere on disk or in the binary. - CredentialSeeder Tool: New developer tool (
tools/CredentialSeeder) used to generate the encrypted credentials file. Run once, commit the output, and all future MSI builds include it automatically.
Changed
- Email sender: Outgoing emails (budget alerts, billing reports) now sent from
Q5000@intellivoss.comviasmtp.hmailplus.com. Authentication uses the master account alias — recipients see the Q5000 address as the sender. - EmailNotificationService: No longer reads SMTP settings from the SQLite database. Credentials are loaded exclusively from the shipped
smtp_credentials.dbfile. - build-msi.ps1: Automatically copies
smtp_credentials.dbinto the publish directory before compiling the MSI. Build warns if the file is missing.
Removed
- Email Settings button: Removed from the Budget Alerts view. SMTP configuration is now managed centrally via the credentials file — end users cannot change email settings.
Version 1.6.5 — 2026-04-24
Security
- SQL Injection Prevention: CDR sync service now validates all dynamic column names against a strict regex whitelist and uses parameterized queries for date values
- Auto-Backup Password Hardening: Auto-backup encryption password is now derived from machine identity (machine name + username via SHA-256) instead of a hardcoded value; eliminates risk of unauthorized decryption with source code access
- Dependency Update: MailKit updated from 4.3.0 to 4.16.0 to resolve a known moderate severity vulnerability (NU1902)
Fixed
- Auto-Backup Restore: Restoring an auto-backup no longer prompts for a password; the correct machine-derived password is supplied automatically based on the filename prefix
- WPF Deadlock Prevention: Replaced
LoadRatesAsync().Wait()in CostCalculationService with a proper synchronous method to prevent potential dispatcher deadlocks - Null Reference in Call Classification:
CallRecord.CallTypeno longer throws whenDispositionis null; safely falls back to empty string - Transactional Restore Operations: All 8 database restore methods in BackupRestoreService are now wrapped in transactions; a failure mid-restore rolls back cleanly instead of leaving partial data
- Silent Migration Failures: DatabaseMigrationRunner now logs all caught exceptions with Serilog instead of discarding them silently
- Email Config Silent Failure: EmailNotificationService now sets a
PasswordDecryptionFailedflag and surfaces a clear validation error when SMTP password decryption fails, rather than sending with a blank password
Improved
- UI Converter Consolidation: Removed duplicate converter definitions from 10 view files; all converters (BooleanToVisibilityConverter, InverseBooleanToVisibilityConverter, InvertBoolConverter) now resolved from a single App.xaml definition
- Button Styling Consistency: Standardized button styles in Budget Alerts, Call Policies, and Policy Violations views to use named styles (InfoButton, SuccessButton, WarningButton, DangerButton)
- Rate File Validation: Rate file parser now validates destination patterns and logs warnings for entries skipped due to missing or invalid data
Updated
- Microsoft.Data.Sqlite: Updated from 8.0.0 to 10.0.5 to align with test project version
Version 1.6.4 — 2026-04-23
Added
- Contextual Help Dialogs: All major views now have a help button (?) in the header that opens a detailed help dialog with descriptions, tips, and usage guidance. Covered views: Dashboard, Reports, Cost Analysis, PIN Management, Departments, Phonebook, Budget Alerts, Billing, Call Policies, Policy Violations, Extension Management, Inbound Analytics, Settings
- PIN License Limit Meter: PIN Management header now shows current PIN count vs license limit with a color-coded progress bar (green/amber/red) and usage text (e.g. "245 / 500 PINs used")
- Extension License Limit Meter: Extension Management header now shows current extension count vs license limit with a color-coded progress bar (green/amber/red) and usage text
Improved
- Limit Meters: Hidden automatically for unlimited licenses; only visible when a license cap applies
Version 1.6.3 — 2026-04-20
Added
- Tax Invoice PDF Generation: Generate professional tax invoices directly from billing runs. Invoices include company header, VAT breakdown, fixed charges, and call billing totals
- Invoice Settings: Configure company details (name, address, VAT number, bank details, payment terms, logo) used on all generated invoices via the new "Configure Invoice Settings" dialog
- Invoice Numbers: Auto-generated sequential invoice numbers assigned when a billing run is invoiced; stored permanently with the run for history and re-printing
- Invoice Detail Modes: Choose between Summary (one line per department/client) and Detailed (per-user breakdown with call counts and durations) when generating invoices
- Invoice Breakdown Storage: Compact department/user breakdown serialised as JSON alongside each billing run, enabling accurate detailed invoice re-generation from history without re-running the billing calculation
- Invoice Status Workflow: Full Draft → Issued → Paid → Cancelled lifecycle with status filter on the Invoices tab
- Bulk Invoice Print: Print all invoices for a filtered selection of billing runs in one action from the Profit & Export tab
Improved
- Hide Rates on Invoice: "Hide Rates" toggle now applies to generated tax invoices in addition to billing reports
Version 1.6.2 — 2026-04-16
Added
- Multi-Server Billing: Billing tab now has a server selector (same pattern as Dashboard). Rate cards, billing runs, and scheduled jobs are filtered per selected server. New runs and rate cards are tagged with the active server ID
- Payment Date Tracking: Marking an invoice as Paid now records today's date; reverting to Issued clears it. Payment Date column added to the invoice grid
- Outstanding Invoices Summary: Amber summary tile at the top of the Invoices tab shows count and total amount of all issued (unpaid) invoices; hidden when none are outstanding
- Fixed Charge Cost Tracking: Each fixed charge entry now has a "Your Cost/Unit" field. This cost is used to calculate accurate profit and margin in billing and profit reports
- Profit Report Export: PDF and Excel profit reports can now be generated from the Billing > Profit & Export tab. A scope selector (All Runs / Invoiced Only / Un-invoiced) filters the export alongside the existing date filter
- Accounting Export Timestamp: Accounting CSV export filenames now include HHmmss to ensure uniqueness
Improved
- Profit Calculation:
BillingLineItem.Profitnow correctly deducts fixed charge costs in addition to call costs, giving accurate margins - Currency in Billing Reports: Billing PDF and Excel outputs now use the configured currency symbol (from rate file settings) instead of hardcoded "R"
- Accounting Export Filter: Export now respects the active search/date filter rather than always exporting all history
Fixed
- Scope label "Reports Only" renamed to "Un-invoiced" for clarity
Version 1.6.1 — 2026-04-10
Added
- Multi-Currency Support: Rate file upload now includes a currency selection step (R/$/€/£/A$/Custom). The selected currency symbol is stored in the database and applied across all reports, billing outputs, dashboards, and cost analysis views
- Live Currency Refresh: Uploading a new rate file with a different currency symbol instantly refreshes all open views without restarting the application
- Billing Fixed Charges: Rate cards now support multiple named fixed charges (e.g., "Line Rental", "Extension Hire") with individual Description, Quantity, and Unit Price fields — replaces the previous single "Fixed Monthly Fee"
- Itemised Billing Reports: PDF and Excel billing reports now display each fixed charge line item individually, with a total per entity
Changed
- Rate File Upload: Now a 2-step process — select currency first, then upload the CSV rate file
- Billing Configuration: Fixed charges are managed as an editable list within the rate card setup, with Add/Remove controls and a running total display
Fixed
- Various internal report generator improvements (InboundAnalytics, InboundOutbound report generators)
Version 1.6.0 — 2026-03-31
Added
- Splash Screen: Application startup with loading progress indicator
- Violations Module: Track call policy breaches with history and reporting
- Call Policies: Configuration UI for defining call policy rules
- Inbound Analytics: Dedicated inbound call reporting and analysis view
- Inbound Analytics Report Type: Specialized report for inbound call metrics
- Phonebook Category Colors: Applied throughout the application for visual clarity
- Comparative Period Reports: Enhanced detailed view comparing periods
- Personal vs Business Charts: Detailed view with individual call charts
- Top Expensive Calls Charts: Added chart views in Summary and Detailed modes
- Total Section: Reports now include totals with view links (Personal vs Business, User Activity)
- Category Filter: Call Category filter for User Activity reports
Improved
- Dashboard: Now displays category colors for better visual clarity
- User Activity Reports: Detailed view includes category colors
- Reports Structure: Better organization with Categories as children of Type
- Report Discovery: Charts now correctly display user names instead of PIN numbers
Changed
- Reports > Categories: Now children of Type (Business/Personal) structure
- Trend Analysis: Currency symbol updated from $ to R
- Trend Analysis Period: Extended from 30 days to 90 days
- Inbound Analytics: Removed *8 prefix from data
Fixed
- Inbound Analytics Data: Removed *8 prefix formatting issue
- Chart Percentages: Fixed to display correctly (e.g., 38.5% instead of 3,846%)
- Chart Titles: Now show department names correctly
- Total Duration: Calculated correctly in Department Reports
- Destination Field: Now correctly populates in call reports
- Department Hierarchy: Child department data rolls up correctly
Version 1.5.0 — 2026-03-24
Added
- PDF Export for Charts: Export chart/graph views to PDF
- Inbound vs Outbound Summary Report: New report type with daily breakdown and pie chart
- Comparative Period Analysis: Compare call metrics across different periods
- Description Notices: Explanatory text for each report type
Improved
- Cost Analysis: Moved into Reports section for unified access
- Reports Order: Summary Report shown first, followed by alphabetical order
- Report Templates: Now save all filter selections correctly
- Filter Display: Reports clearly show all currently selected filters
Fixed
- Destination Field: Now correctly populates in all call reports
- Department Hierarchy: Parent departments correctly roll up child department data
Version 1.4.1 — 2026-03-18
Added
- Call Category Filter: Filter User Activity reports by call category
- TOTAL Row: User Activity reports now include totals with view link
- Top N Users Chart: Click view link on TOTAL row to display percentage breakdown
- Percentage Breakdown Chart: Visual distribution of users in filtered results
Improved
- Chart User Names: Charts now correctly display user names instead of PIN numbers
- Report Flexibility: TOTAL section works with any filter combination
Version 1.3.7 — 2026-03-15
Improved
- Update Check Timing: Moved from 2 seconds to 10 minutes after app startup
- Update Check Rate Limit: Reduced from 24 hours to 6 hours between checks
- Update Notifications: Users get update notifications even with brief app usage
Version 1.3.6 — 2026-03-14
Fixed
- Critical Installer Bug: Installer now correctly updates application files (not just metadata)
- Build Process: MSI build now rebuilds application before packaging
- Version Verification: Added version check during MSI build process
Note
- All users on v1.3.0–1.3.5 should update to ensure future updates work correctly
Version 1.3.5 — 2026-03-13
Added
- Department-Specific Top N Charts: View Top N users for each department in Detailed mode
- View Links: Smart link visibility - only shows for relevant rows
- TOTAL Row: Added to Detailed view mode
- Fetch All Calls: Removed 100-call limit - now fetches all calls in date range
Improved
- Chart Accuracy: Percentages now display correctly (e.g., 38.5% not 3,846%)
- Chart Titles: Now show department names
- Total Duration: Calculated correctly
Version 1.3.1 — 2026-03-10
Fixed
- Sync Status Detection: Fixed orphaned sync status detection issue
- App Shutdown: Improved cleanup - no lingering processes on shutdown
Version 1.3.0 — 2026-03-09
Added
- Department Management: Enhanced department tracking and reporting
- Department Hierarchy: Parent/child department relationships
- Department Reports: New report views for department analysis
Version 1.2.1 — 2026-03-09
Added
- UI Improvements: Enhanced user interface and usability
Changed
- Update System: Refinements to auto-update mechanism
Version 1.2.0 — 2026-03-08
Added
- Auto-Update System: Automatic application update checking
- License Management: License validation and tracking
- Update Notifications: User notifications for available updates
Version 1.0.0 — 2026-03-08
Added
Core Features
- Dashboard: Real-time overview with summary tiles, call statistics, and budget indicators
- PIN Management: Map account codes to users and departments
- CRUD operations for PIN mappings
- CSV import/export
- Bulk operations support
- Search and filtering
- Department Management: Organize users into departments with budget tracking
- Department CRUD operations
- Budget allocation and monitoring
- Cost center tracking
- Department hierarchy support
- Extension Mapping: Map PBX extensions to users and departments
- Extension CRUD operations
- Automatic detection from CDR data
- Conflict resolution
- Phonebook: Comprehensive contact management
- Contact CRUD operations
- Categories: Supplier, Employee, Client, Personal
- CSV import/export
- Number matching for call classification
- Cost Rules: Define call cost calculations
- Pattern-based rules (regex/prefix matching)
- Rate card management
- Priority-based rule evaluation
- International rate support
- Budget Alerts: Proactive budget monitoring
- Configurable threshold alerts (80%, 90%, 100%)
- Email notifications
- Alert history and tracking
- Acknowledgement system
Reporting
- PDF Reports: Professional formatted reports with charts and company branding
- Daily Summary Report
- Department Summary Report
- User Activity Report
- Call Detail Report
- Cost Analysis Report
- Cost Verification Report
- Personal vs Business Call Report
- Trend Analysis Report
- Excel Export: Formatted spreadsheets with charts and formulas
- All report types available as Excel
- Pivot-ready data format
- Custom styling and formatting
- Report Templates: Pre-built and customizable report configurations
- Save custom filter combinations
- Scheduled reports (future)
- Distribution lists (future)
Analytics & Visualization
- Call Analysis Charts: OxyPlot-powered visualizations
- Call volume by hour/day/week
- Cost trends over time
- Department spending comparison
- Top N reports (expensive calls, long calls, frequent destinations)
- Busy hour analysis
- Budget Gauges: Visual budget tracking with red/amber/green indicators
- Interactive Dashboards: Real-time data updates
PBX Integration
- SSH Connection: Secure SSH tunneling to PBX
- Support for key-based and password authentication
- Connection testing and validation
- Automatic reconnection
- CDR Retrieval: MySQL database integration
- Support for FreePBX/Issabel/VitalPBX CDR schemas
- Date range filtering
- Incremental updates
- Large dataset handling (100K+ records)
- Real-time Monitoring: Dashboard auto-refresh
Technical Features
- Self-Contained Deployment: No .NET installation required
- Includes full .NET 8 Desktop Runtime
- ~250 MB published size
- Single MSI installer (~150-180 MB)
- Database: Local SQLite for configuration and cached data
- AES-256-GCM encryption for credentials
- Automatic migrations
- Performance optimizations
- Logging: Comprehensive logging with Serilog
- File-based logging
- 7-day rolling retention
- Debug and error level separation
- Material Design UI: Modern, business-friendly interface
- Dark theme (Navy Blue + Cyan accents)
- Responsive layouts
- Intuitive navigation
- Error Handling: Graceful error handling and user feedback
- Detailed error messages
- Crash reporting (optional)
- Recovery mechanisms
Security
- Encryption: AES-256-GCM for all stored credentials
- SSH Tunneling: All database access via SSH (no direct MySQL)
- Secure Storage: Credentials never in plaintext
- Input Validation: SQL injection prevention
- Audit Logging: Track configuration changes
Performance
- Caching: Smart CDR caching for offline access
- Lazy Loading: On-demand data loading for large datasets
- Async Operations: Non-blocking UI with async/await patterns
- Query Optimization: Efficient SQL queries with indexing
- ReadyToRun: Pre-compiled for faster startup
Documentation
- User Guide: Comprehensive PDF documentation
- Quick Start Guide: 2-page getting started guide
- In-App Help: Context-sensitive help system
- Administrator Guide: Deployment and configuration guide
- Release Notes: Detailed changelog
Deployment
- WiX Installer: Professional MSI installer
- Windows 10/11 support (64-bit)
- Automatic upgrades
- Start Menu and Desktop shortcuts
- Add/Remove Programs integration
- Clean uninstall
- Code Signing: Digitally signed binaries (when configured)
- Auto-Update: Update checking infrastructure (future activation)
- Version Management: Centralized version.json system
Dependencies
- .NET 8.0 (included in self-contained deployment)
- MaterialDesignThemes 5.0.0
- CommunityToolkit.Mvvm 8.2.2
- OxyPlot.Wpf 2.1.2
- QuestPDF 2024.12.3
- ClosedXML 0.104.2
- SSH.NET 2024.1.0
- MySqlConnector 2.5.0
- Serilog 4.3.0
Known Limitations
- Single PBX support only (multi-PBX planned for v2.0)
- Windows-only (WPF application)
- Manual CDR sync (no real-time CDR updates)
- Email alerts require SMTP configuration
- No mobile app (planned for v2.0)
Breaking Changes
- N/A (initial release)
Deprecated
- N/A (initial release)
Removed
- N/A (initial release)
Fixed
- N/A (initial release)
Version Guidelines
Version Number Format
- Major: Breaking changes, major new features (e.g., 1.0.0 → 2.0.0)
- Minor: New features, non-breaking changes (e.g., 1.0.0 → 1.1.0)
- Patch: Bug fixes, minor improvements (e.g., 1.0.0 → 1.0.1)
Change Categories
- Added: New features
- Changed: Changes to existing functionality
- Deprecated: Features marked for removal in future versions
- Removed: Features removed in this version
- Fixed: Bug fixes
- Security: Security-related changes
Release Links
- [1.0.0] - Initial Release
For support, visit: https://q5000.com/support