All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
[Unreleased]
[2.7.2] - 2026-08-23
Added
- SIP trunk
[identify]escape hatch:custom_identify_settings—SIPPeergained a free-text field for the body of the generated[identify]section (migration0081_sippeer_custom_identify_settings.py), exposed in the admin’s “Advanced” fieldset next tocustom_aor_settings.core/conf.py’s[identify]generation previously only supported IP-basedmatch, which can’t distinguish multiple lines behind the same source IP — e.g. a GSM gateway where each of 4 ports registers from the same address and inbound calls only carry a distinguishingContact: 0001header. When the field is set,__section_trunk_endpoint()also derivesidentify_byfrom its content instead of the old singleidentify_by=ipcheck:headerif the text containsmatch_header,ipif it containsmatch=, plususernamewheneverregistrationHereis enabled. This isn’t cosmetic — Asterisk’sidentify_bydefault (username,ip) has noheadervalue, and REGISTER-to-AOR resolution requires listing a method Asterisk documents as AOR-capable (onlyusernamequalifies among the ones used here), so a header-matched trunk that also registers needs bothheader(for inbound INVITEs, since From/To carry real phone numbers, not the port id) andusername(for REGISTER, since the gateway’s own username per line does match the AOR name) at once. Trunks that leave the field empty are unaffected —identify_by=ipis still emitted exactly as before. Covered by new tests incore/tests.py. - REST API: pause/unpause and list queue members —
POST /api/v1/queues/members/pause/sends AMIQueuePausefor a giveninterface(optionally scoped to onequeue), andGET /api/v1/queues/members/(optional?queue=<name>) returns live member status (paused, status, calls taken, …) via AMIQueueStatus. Both talk to Asterisk directly, independent of the dashboard’s Redis-backed state, so they work even withoutdashboard-listenerrunning.core/ami.AsteriskManagementInterfacegainedqueue_pause()andqueue_members(); the dashboard’s existingpause_queue_member()view now goes through the same wrapper instead of opening its own AMI connection. New validatorcore.validators.validate_asterisk_interfaceguards theinterfacefield against AMI injection. AMI timeouts (all in seconds) are now named settings instead of scattered magic numbers:ASTERISK_AMI_DEFAULT_TIMEOUT(long-running admin actions like reload/restart),ASTERISK_AMI_QUICK_TIMEOUT(pause/status/hangup-style actions),ASTERISK_AMI_RESPONSE_MARGIN(slack added on top of an Originate’s owntimeout_ms). - Docker integration test suite against a real Asterisk —
tests/integration/test_queue_members.pyseeds a real Queue + PJSIPQueueMember, applies the generated config the same way “Apply Changes” does, reloads a liveandrius/asterisk:22container, and drivesPOST /api/v1/queues/members/pause//GET /api/v1/queues/members/against it — confirming pause state actually changes in Asterisk, not just that the right AMI calls were made. New isolated stackdocker-compose.integration.yml(own volumes/DB/network, no host ports), run viamake integration-test. Opt-in in CI (.github/workflows/integration-test.yml, push to main/master or manual dispatch) rather than on every PR, since it takes tens of seconds versus the existing mocked suite’s seconds. - CRM webhooks: separate outgoing-call event chain — new events
call.outgoing/call.outgoing_answered/call.outgoing_ended, fired for calls placed by a SIP user (never a trunk), independent of the existing inbound chain (call.incoming/call.answered/call.missed/call.ended).Webhookgainedsend_outgoing,send_outgoing_answered,send_outgoing_ended(migration0005_webhook_outgoing_events.py); each requires at least one selected routing table, mirroring how inbound events require a context or queue. New payload fieldsdirection("inbound"/"outbound"),dest_channel,dial_status,answered. Outgoing-call detection is endpoint-based, not context-based: both a SIP user and a trunk can end up with a PJSIP context equal to a routing table’s name, soapps/webhooks/sync.pynow serializes asip_usersmap ({endpoint: routing_table}, SIP users only) intowebhooks:config, andwebhook_sender.extract_endpoint()resolves the AMI channel’s endpoint against it — a trunk’s channel never matches, even sharing a routing table’s name with a SIP user.services/dashboard/dashboard_listener.pywiresAMI DialEnd(DialStatus=ANSWER) tocall.outgoing_answered, closing a gap noted in the previous release (direct/non-queue calls previously had no “answered” signal at all). - CRM webhooks:
linkedid,channel,channel_vars; system-channel filter — every event now carrieslinkedid(Asterisk’s own cross-channel call-correlation id, read from AMILinkedid) andchannel(the channel name), so a CRM can group separate webhook deliveries belonging to the same logical call without guessing fromuniqueidproximity or timestamps. Newchannel_varsobject exposes allow-listed Asterisk channel variables (envWEBHOOK_CHANNEL_VARS, defaultULINE— e.g.services/fastagi’s parking-slot allocator), tracked via the existingVarSethandler and echoed on every event where the variable is already known.call.outgoing(and itsoutgoing_answered/outgoing_endedchain) is now skipped by default for a channel Asterisk created viaDial()/Originate()that has not yet beenGoto()’d to a real extension (extenstill the dialplan placeholder"s") — pure system noise for a CRM; set envWEBHOOK_SEND_SYSTEM_CHANNELS=trueto restore the old behavior. Migration0007_add_correlation_fields_to_templates.pyupgrades unmodified defaultpayload_templaterows to include the three new placeholders, leaving customized templates untouched.
Changed
- BREAKING for existing
Webhookrows that userouting_tables— before this release,routing_tableswere merged into the samecontextslist used forcall.incomingmatching (added in 2.7.1), so a webhook configured with only a routing table firedcall.incoming/call.endedfor outbound calls. Migration0006_migrate_routing_table_webhooks.pytranslates existing rows automatically (send_outgoing = send_incoming,send_outgoing_ended = send_ended; inbound flags are cleared only if the row had nocontexts/queuesof its own), but the CRM-side handler must be updated to expectcall.outgoing*instead ofcall.incoming/call.endedfor those calls. Deploy order:migrate+sync_webhooksfirst, then restartdashboard-listener— an old listener process doesn’t know the newwebhooks:configkeys (sip_users, separaterouting_tables) and won’t fire the outgoing chain during the rollout window. apps/webhooks/models.Webhook.routing_tableshelp text and admin validation updated to reflect that routing tables now filter only the outgoing chain, never the inbound one.
[2.7.1] - 2026-08-11
Added
- CRM webhooks: outbound calls now match via routing table —
Webhookgained arouting_tablesM2M field (migration0004_webhook_routing_tables.py); a SIP user’s PJSIP context is its routing table’s name, so outbound calls were never matched by webhooks that only listed inboundDialplanContexts.apps/webhooks/sync.pymerges routing table names into the samecontextslist sent to the dashboard listener; admin form, m2m signals, and tests updated accordingly. - Public release files —
LICENSE(PolyForm Shield 1.0.0 — permits any use, including commercial, except operating a competing product/service),NOTICE,CONTRIBUTING.md,QUICKSTART.md(Ansible and Docker Compose install paths), SPDX headers (LicenseRef-PolyForm-Shield-1.0.0) incore/models.py,core/conf.py,manage.py,pbx/settings.py. - Full Docker Compose stack — new
services/fastagi/Dockerfile,services/dashboard/Dockerfile,services/callback/Dockerfile;docker-compose.ymlgainedfastagi,dashboard-listener,callback-service(opt-in via--profile callback), andasterisk-init, which seeds a minimal AMI-enabledmanager.confand patchesmodules.confto loadres_crypto.sobefore Asterisk’s first boot — the vendorandrius/asterisk:22image ships both AMI disabled andres_cryptounloaded (the latter breaks the image’s own healthcheck).docker-entrypoint.shrunsmigrate/collectstaticidempotently on everydjangocontainer start.docker-compose.override.ymlgivesdjangoa source bind mount +uvicorn --reloadfor local development, picked up automatically bydocker compose up. PARKING_ULINE_MIN/PARKING_ULINE_MAXexplicitly declared inpbx/settings.py(previously only read viagetattr(..., default)inapps/dashboard/views.py); documented inenv.sample.FASTAGI_HOST/FASTAGI_PORTenv vars forservices/fastagi/fastagi.py— the server previously always bound127.0.0.1:4573, unreachable from a separate Asterisk container; defaults unchanged for existing bare-metal/systemd installs.
Changed
- License: the project is released under PolyForm Shield 1.0.0 (source-available) — permits any use, including your own commercial deployment, except building a competing product or service on the code. See
LICENSE/NOTICE/README## License. - README rewritten: license badge + Quick Start badge,
## Quick Startnow points toQUICKSTART.md,## Licenseand## Contributingsections rewritten for the new license andCONTRIBUTING.md. DJANGO_SECRET_KEYgeneration hint switched from a Django management-command one-liner toopenssl rand -hex 50(env.sample,pbx/settings.py) — the former assumed Django was already installed locally, which isn’t true for the Docker-first setup path..gitignore: added.env,*.env,staticfiles/,*.pyc,.idea/,local_settings.py,tasks/.docs/reorganised:CHANGELOG.mdmoved to the repository root;docs/en/install_asterisk.mdanddocs/en/INSTALL.mdremoved (superseded byansible/install.yml, which already automates everything they described by hand — including the/etc/asteriskownership fix);docs/en/realtime_in_future.mdmoved to the (git-untracked)tasks/directory as an internal roadmap note.tasks/itself removed from git tracking (internal planning docs, not part of the public release) — files kept on disk.
Fixed
- Static files not served under Docker —
DEBUGisFalseeven inDEVMODE=Developmenthere (onlywithout_asterisk_on_localhostflips it), so Django wasn’t serving/static/itself and there is no nginx in front of thedjangocontainer in Docker Compose. Addedwhitenoise+WhiteNoiseMiddleware; no effect on bare-metal/Ansible installs where nginx already serves/static/first. djangoserviceREDIS_URLmisconfigured indocker-compose.yml— it was set via unusedREDIS_HOST/REDIS_PORTvariables (a pattern only the standalone services read); Django itself only reads a singleREDIS_URL, which was silently falling back toredis://localhost:6379inside the container. Fixed toREDIS_URL=redis://redis:6379; this was breaking the/dashboard/ulines/page and would also have broken the WebSocket dashboard (channels_redisuses the same setting).- Broken
docs/openapi.yamllink in README — the file lives atdocs/en/openapi.yaml; README now links to bothdocs/en/API.mdanddocs/en/openapi.yaml.
[2.7.0] - 2026-08-10
Added
- AEL global variables — new
DialplanGlobalVariablemodel (core/models.py, migration0080) lets an admin define namedglobals { }entries via the Django admin, emitted at the top of the generatedextensions.aelbymake_dialplan_globals()incore/conf.py. Name/value are validated with newvalidate_ael_variable_name/validate_ael_variable_valuevalidators incore/validators.py(identifier syntax; no;or line breaks in the value). Covered by new tests incore/tests.py. - Calls by Destination Number report —
AnalyticsDestinationCallsView(apps/reports/views.py), templateanalytics_destination_calls.html, andAnalyticsDestinationCallsForm, registered at/reports/analytics/destination-calls/. Counts answered/total external inbound calls grouped by destination (B-number) — inbound leg must belong to aSIPPeer— with unique-caller counts, average talk time, filters for destination/exclude-contacts/top-N, and CSV export. Gated byview_analytics_reports. Translations updated for en/es/uk. backup_asterisk.sh— dailytar.gzbackup of/etc/asteriskwith configurable retention (RETENTION_DAYS, default 14 days) and a Slack alert on failure. Configured via/etc/PearlPBX/backup_asterisk/env(templated frombackup_asterisk.env.j2in thesystemAnsible role), backing up into/var/backups/asterisk-etc; installed as a daily cron job (02:30) by thepearlpbx2role.
Changed
- English-only API responses — new
core.middleware.ForceEnglishAPIMiddlewareforces theenlocale for any request under/api/, regardless ofAccept-Languageor the caller’s session locale, so external integrations get stable English messages. - Channel-classification helpers extracted — new
apps/reports/services/channels.pymodule (peer_channel_regex()/user_channel_regex()) replaces the inline regex-building previously duplicated inCDRReportView; regexes are now computed lazily and memoized, and an empty SIPPeer/SIPUser list yieldsQ(pk__in=[])instead of a malformed regex. pg_backup_pearlpbx2moved fromcron.dailyto an explicit cron entry — now runs daily at 01:30; the legacy/etc/cron.daily/pg_backup_pearlpbx2script is removed during install.syncmp3.sh—BACKUP_MP3_DAYS=0now disables backup-directory cleanup instead of deleting every file in it.
Fixed
- CRM webhook
call.answered/call.missedmissing destination number —services/dashboard/webhook_sender.py’s_on_agent_connect/_on_abandonnow enrich their payloads withexten/contextfrom thewebhook:notified:{uniqueid}marker (the same data already included incall.incoming/call.ended), so CRM integrations can see which number the caller dialed for answered/missed queue calls too. Covered by new tests inservices/dashboard/tests.py. - Ansible update playbook —
manage.py showmigrations/migrate/collectstaticsteps inansible/update.ymlnow run with--skip-checks, so Django system checks unrelated to the update (e.g. warnings from in-progress model changes) no longer abort the update process. - Env-file parsing in Ansible update/rollback —
ansible/update.ymlandansible/rollback.ymlno longer read/etc/PearlPBX/PearlPBX2/envviaslurp+ base64 decode;update.ymlnow useslookup('file', …)with a regex that correctly handles digits in variable names and strips surrounding quotes from values.
[2.6.0] - 2026-07-24
Added
- Ansible rollback procedure —
rollback.shplusansible/rollback.ymlandbin/pearlpbx2_resolve_rollback_target.pylet an admin roll a deployment back N steps using a deploy-state ledger written byupdate.yml: it resolves the target commit/migration state, reverts Django migrations while the newer migration files are still on disk, checks out and syncs the rolled-back code (aborting if the source repo has uncommitted changes), reinstalls dependencies for the main app and thecallback/dashboard/fastagiservices, and restarts them.ansible/update.ymlnow also ensures the deploy-state directory exists before writing the ledger. sync_manager_usersmanagement command (core/management/commands/sync_manager_users.py) — creates/updates the AMIManagerUsersrecords for the built-incallback,dashboard_listener, andfastagiservices from fixed scopes, mirroringmanager.conf.j2; wired into the Ansiblepearlpbx2role install so these accounts are (re)provisioned automatically instead of requiring manualmanager.confedits. Covered by new tests incore/tests.py.- Token authentication for dashboard WebSocket and read-only JSON API —
apps/dashboard/consumers.py’sAsteriskEventsConsumer.connect()now also accepts a DRF auth token (via?token=query param orAuthorization: Token <key>header) when there is no authenticated session; a matchingtoken_or_login_requireddecorator inapps/dashboard/views.pyextends the same fallback to the read-only endpoints (get_sip_endpoints,get_queue_state,get_all_queues,get_all_channels,get_channel,get_active_calls,get_missed_calls,get_channels_by_type), enabling CRM/external integrations that can’t rely on Django sessions.docs/ua/crm-integrator-guide.mdupdated accordingly.
Changed
ALLOWED_HOSTSduring install now includes the host’s FQDN,localhost, and all detected IPv4 interface addresses (in addition to the short hostname and127.0.0.1), so the admin UI is no longer rejected with a 400 when reached via a secondary network interface’s IP.CHANNEL_LAYERSRedis backend given explicitcapacity(256) andexpiry(60s) settings instead of relying on channels_redis defaults.- Ansible install playbook simplified — removed the interactive timezone-confirmation preflight step and the PostgreSQL system-timezone-sync task (
ansible/install.yml,ansible/roles/postgres/tasks/main.yml); theasteriskrole now pre-seeds/etc/asteriskwith PearlPBX2’s own baseline configs before runningmake basic-pbx, so Asterisk’s demo queues/agents are never installed in their place;collectstaticnow runs with AMI connection settings andCSRF_TRUSTED_ORIGINSin its environment.
Added
- CRM webhooks (
apps/webhooks/) — newWebhookmodel lets an admin register one or more CRM endpoints that receive JSON POST notifications for call events (incoming, answered, ended/missed), driven fromservices/dashboard/dashboard_listener.pyvia the newservices/dashboard/webhook_sender.py. Payloads are built from a configurable, validatedpayload_template(JSON object with${variable}placeholders such ascaller_id_num,queue,recording_url,wait_time, etc.), added in a follow-up migration (0003_alter_webhook_payload_template.py). Async_webhooksmanagement command andapps/webhooks/sync.pykeep webhook definitions in sync. Documented indocs/ua/crm-integration.mdanddocs/ua/crm-integrator-guide.md. - Call recording lookup API —
GET /api/v1/recordings/<uniqueid>/(apps/api/views/recordings.py, backed byapps/reports/services/recordings.py) serves the recorded audio (wav/mp3, Range-request support) for a given Asterisk uniqueid; this is the deterministic URL delivered to CRMs asrecording_urlin webhook payloads. - ConfBridge conference calling — new
POST /api/v1/calls/conference/endpoint (apps/api/views/calls.py,ConferenceSerializer) originates multiple parties (e.g. operator, client, driver) concurrently into a shared ConfBridge room, alongside the existing single-leg/calls/originate/.core/ami.pygained non-blockingsend_originate()split out fromoriginate()so conference legs dial in parallel instead of sequentially.core/conf.py/pbx/admin.pygenerateconfbridge.confdefault profiles; a reserved"conference"DialplanContext/DialplanExtension(migration0079) and acore/checks.pysystem check prevent an admin from creating a colliding context or routing table name.core/validators.pywhitelists ConfBridge in the dialplan AEL validator. - Repository docs reorganised under
docs/en/(English) anddocs/ua/(Ukrainian); newdocs/en/realtime_in_future.md,docs/ua/crm-integration.md,docs/ua/crm-integrator-guide.md.bin/rename_pearlpbx2_services.shadded to rename legacy systemd units to thepearlpbx2-*naming scheme.
Fixed
- PJSIP outbound registration AOR contact —
__build_aor_contact_line()incore/conf.pynow falls back toregistration_uriwhencontact_uriis not set (with a warning, since registrar and media host may differ), instead of silently emitting no contact. For trunks withregistrationThere(registering to a remote provider), the AOR now seeds a static bootstrap contact so calls aren’t blackholed in the window before the first successfulREGISTER; once registration succeeds,remove_existing=yesreplaces it with the learned contact.
[2.4.0] - 2026-07-16
Added
- Slack notifications for missed queue calls — the dashboard listener (
services/dashboard/) can now optionally send an aggregated Slack message when callers abandon a queue. All abandons within a configurable debounce window (default 60 s) are grouped into a single message per queue. Configure viaSLACK_MISSED_CALL_WEBHOOK_URLandMISSED_CALL_DEBOUNCE_SECONDSinservices/dashboard/env. Feature is off by default (empty webhook URL). - Classic AGI scripts —
services/agi/now shipsmissed_call.pyandunmatched_call.pyfor per-call Slack notifications from Asterisk dialplan, plus a sharedagi_common.pylibrary withnotify_slack()helper. Config at/etc/PearlPBX/AGI/env. - REST API migrated to Django REST Framework —
apps/api/endpoints for lists/blacklist/whitelist/contacts are now DRFViewSets registered viaDefaultRouter, with proper serializers (apps/api/serializers.py) replacing hand-rolledJsonResponseviews. New call-control endpoints underapps/api/views/calls.py, a machine-readabledocs/openapi.yamlspec, anddocs/API.mdupdated to match.AGENTS.mdadded documenting the API for agent/automation consumers. - Pause/unpause queue members from the dashboard — the new live dashboard (
apps/dashboard/views.py,new_dashboard.html) can send AMIQueuePause/QueueUnpausefor a given interface directly from the Queues tab; restricted to staff users. - Queue member import management commands —
core/management/commands/add_queue_members.pyandimport_pbx1_users.pyfor bulk-importing queue members and migrating users from a legacy PBX1 install. manage.shhelper script for common day-to-day operational commands, alongside improvements toinstall.sh/update.sh.- Extended SIP Peer (trunk) configuration —
host_portreplaced with three explicit fields:registration_uri,contact_uri, andmatch_hosts(migrations0069–0072), giving independent control over the registration server, AOR contact, andidentifymatch hosts. Addedcontact_userandauth_type(plaintext/MD5) fields (migration0073) for trunks that require a distinct contact user or MD5 authentication. - Admin warning for skipped PJSIP users — the Apply Changes page now lists SIP users excluded from
pjsip.confgeneration (e.g. due to validation failures) so misconfigurations aren’t silently dropped. bin/asterisk_logrotate.sh— daily logrotate script for Asterisk logs, wired into theasteriskAnsible role.system_monitor.shstartup Slack alert — sends a one-time “monitoring started” message to Slack on first run per host.- Ansible: PostgreSQL timezone configuration during install, and a new
manager.conf.j2template plus expandeddocs/admin-guide.md/docs/user-guide.md. - Queue config generation now emits
maxlen,weight,setqueuevar,random-periodic-announce, andforce_longest_waiting_calleroptions inqueues.conf.
Fixed
- CDR report timestamps —
CDRReportViewno longer raises when a stored datetime is naive; it now falls back to formatting it as-is instead of only handling timezone-aware values. - Permission checks in
apps/reports/mixins.pysimplified and corrected — removed ~70 lines of redundant/incorrect logic. queues.confannounce-holdtimenow emitted as its configured numeric value instead of being coerced to a boolean.- PostgreSQL backup script renamed and fixed —
bin/pg_backup_asterisk.sh→bin/pg_backup_pearlpbx2.sh, with corrected environment handling in thesystemAnsible role. wav2mp3_monitor.shand related scripts no longer redirect their own output into the log they monitor;pg_backup/syncmp3/system_monitorscripts had assorted reliability fixes.custom_list_namestable reference typo inservices/fastagi/fastagi.pyfixed (was querying the pluralcustom_lists_names).- Callback scheduling — FastAGI callback insert now uses
make_interval()instead of string-interpolating the delay into theINTERVALliteral. ConfigurationFileAdminno longer silently discards name/description/path edits when file content is unchanged — it now saves the row and informs the admin that no new version was created.DialplanContext/RoutingTableuniqueness — addedclean()validation (in addition to the existingsave()check) so the “name already used in the other model” conflict surfaces as a normal form error.- Phone provisioning config directory auto-created if missing when writing a device config file; removed a dead/no-op
apply_all_configurationsview fromapps/provision/. HomepageStatusViewAMI client is now always logged off viafinally, even when connecting or querying it raises.MusicOnHold.mode/.sortdefaults corrected to use the actual enum members instead of a raw integer (migration0078).
Changed
SIPUser.md5_crednow computes HA1 asMD5(username:realm:password)per RFC 2617 (previously the realm and password were swapped).AllowedHostsIPMixin.get_client_iponly trustsX-Forwarded-Forwhen the direct peer’s address is listed in the newPEARLPBX_API_TRUSTED_PROXIESsetting; otherwise it falls back toREMOTE_ADDR, closing an IP-allowlist bypass via a spoofed header.ApplyChangesViewconfig file paths are now built withos.path.join/os.path.normpathand validated to stay insideASTERISK_ROOT_DIR, rejecting a craftedConfigurationFile.paththat could otherwise escape the sandbox; the AMI connection used to apply changes is now always logged off in afinallyblock.PasswordWithToggleInputwidget escapes field name/value/attrs before interpolating into HTML, and its “generate password” JS now draws fromcrypto.getRandomValuesinstead ofMath.random().SIPUser.secret/ManagerUsers.secretno longer require database-level uniqueness (removedunique=True) — multiple endpoints legitimately sharing a secret is not an error condition.- No committed default secrets —
ASTERISK_MANAGER_SECRETandDJANGO_SECRET_KEYno longer ship with a real fallback value; both are required via environment variables in any network-reachableDEVMODE(Development/Staging/Production) and raiseImproperlyConfiguredif missing, falling back to an obviously-fake value only forwithout_asterisk_on_localhost. - Cookie security flags (
SESSION_COOKIE_SECURE,CSRF_COOKIE_SECURE) are now also enforced in Staging, not just Production. - Default log level lowered from DEBUG to INFO for
core/apps/__main__loggers to avoid leaking AMI event payloads (caller IDs, PII) into journald by default. /moh/<path>static file serving now requires an authenticated session (login_required), since the MOH tree is writable by admins.- Dashboard queue-control endpoints (
hangup_channel,pause_queue_member) now requirerequest.user.is_staff, not just login. merge_args_envin the callback and dashboard services now correctly prioritises CLI arguments over environment defaults (previous logic had the priority inverted).features.confdefault template simplified/updated.- Ansible:
asteriskrole now manageslogger.confdefaults and logrotate;ansible.cfgandupdate.shfixes for the update playbook.
Security
- TLS/AMI/API hardening pass (“fix the critical/high/medium issues” commits) covering: IP-spoofing via
X-Forwarded-For, HA1 credential hash order, committed default secrets for AMI and Django, insecureMath.random()password generation, unescaped HTML in the password widget, a path-traversal vector in Apply Changes’ config writer, missing staff checks on dashboard AMI control endpoints, dialplan validation bypass on programmatic writes (management commands/imports), and open static-file serving of the MOH directory. See details above under Fixed/Changed.
[2.3.3] - 2026-05-24
Security
- TLS private key TOCTOU race fixed —
_write_cert_filenow creates files with0o640permissions atomically viaos.open(), eliminating the window where a private key was world-readable betweenopen()andchmod(). - Path traversal in cert filenames blocked —
SIPTransport.namenow validates against[a-zA-Z_][a-zA-Z0-9_-]*(migration0068);_write_cert_filealso strips path components withos.path.basename()as a second line of defence.
Fixed
- TLS cert writes no longer trigger on config preview —
make_pjsip_conf_transports()is now a pure function (string only); cert files are written by the newwrite_tls_cert_files()called exclusively during Apply Changes (POST), not on the preview GET request. cert_write_dirpath normalisation — replaced raw string concatenation withos.path.normpath()to handle trailing slashes inASTERISK_ROOT_DIRcorrectly.
Added
verify_serverandallow_reloadTLS transport options — new boolean fields onSIPTransport(migration0067) emitted topjsip.conffor TLS transports.- TLS certificate content stored in DB —
cert_file,priv_key_file,ca_list_filecontents are now written toASTERISK_CONFIG_DIR/certificate/on Apply Changes.
[2.3.2] - 2026-04-27
Fixed
- Queues.conf generation - ringinuse and timeoutrestart was generated incorrect.
[2.3.0] — 2026-04-12
Added
- Queue agent info in dashboard call modal — when a bridged call involves a queue agent, the agent name and extension are now shown in the call details modal.
- Dashboard agent panel improvements — queue agents tab enhanced with clearer status indicators and layout tweaks.
- Agent state restoration after Asterisk restart — the dashboard service now automatically re-queues paused agents when Asterisk reconnects, so pause states survive restarts.
- AMI timeout configurable — callback service
--ami_timeoutflag added; default raised to 60 s for more reliable trunk connections. - Queue activity report — exclude contacts filter — the queue analytics report now supports filtering out specific contacts from results.
- Link from missed-calls analytics to queue log — the missed-calls report now includes a direct link to the queue activity log for the relevant queue.
Fixed
- Callback service reliability — extensive rework: removed auto-reconnect loop (service now exits cleanly on AMI disconnect and relies on systemd restart), improved error handling and state machine for outbound callback calls.
- Callback report links — fixed broken links between the callback report view and individual callback records.
- PJSIP
identifysection removed when IP:Port not defined —core/conf.pyno longer emits an emptyidentifyblock for endpoints without a static IP, preventing Asterisk config warnings. - CSS for wide screens (1600 px+) — dashboard layout no longer overflows on large monitors; responsive breakpoints added.
Changed
- Dashboard improvements — new API endpoint
GET /dashboard/api/queues/supplies queue member data; dashboard listener publishes richer queue-member events to Redis; CDR report row link updated for consistency. ManageUsers.write_timeoutmigration — corrected default value in migration0065.
[2.2.0] — 2026-03-29
Added
- New live operator dashboard (
/dashboard/live/) — fully rewritten dark-theme dashboard with five tabs: Overview, Queues, PJSIP, Bridged, Channels. - WebSocket connection status indicator (LED pill showing CONNECTING / LIVE state).
- Real-time summary stats: active channels, bridged pairs, queues, waiting callers.
- Old dashboard preserved and accessible at
/dashboard/old/;/dashboard/now redirects to the new dashboard. GET /dashboard/api/endpoints/— returns lists of internal SIPUser usernames and external SIPPeer names from the database, used by the frontend to classify PJSIP channels.POST /dashboard/api/channels/hangup/— terminates a call via AMI; CSRF-protected, channel name validated against allowlist regex.
Fixed
- CDR report — outgoing direction filter: the query now correctly matches both
channel(user pattern) anddstchannel(peer pattern). Previously onlychannelwas checked, causing outgoing calls to trunk to be missed or incorrectly included. - Audio player now stops automatically when the CDR details modal is closed.
Changed
- CDR table: a “click for details” hint appears on row hover to improve discoverability.
- Home page queue list: rebuilt with CSS Grid (3 columns: name / waiting badge / agents) replacing the previous flex layout.
- Media player progress bar: click target enlarged to 20 px tall while the visible track remains 6 px with a background rail, improving seek accuracy.
[2.1.4] — 2025-04-01
Previous stable release. See git history for details.