This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
ModuleAutoprovision is a MikoPBX extension module for automatic IP phone provisioning. It discovers phones on the local network via PnP multicast (224.0.1.75:5060), generates vendor-specific configuration files, and delivers them over HTTP. Supported vendors: Yealink, Snom, Fanvil, Grandstream.
/Users/nb/PhpstormProjects/mikopbx/MikoPBXUtils/node_modules/.bin/babel \
public/assets/js/src/module-autoprovision-index.js \
--out-dir public/assets/js/ \
--source-maps inline \
--presets airbnbphpstan analysePushes to master or develop trigger .github/workflows/build.yml, which uses the shared mikopbx/.github-workflows extension-publish workflow (initial version: 1.62).
All PHP classes use Modules\ModuleAutoprovision\ namespace with PSR-4 autoloading rooted at /.
MVC (Phalcon Framework):
App/Controllers/ModuleAutoprovisionController.php— Web UI controller (settings tabs: phones, templates, URIs, phonebook, PnP config)App/Forms/ModuleAutoprovisionForm.php— Form field definitionsApp/Views/index.volt— Phalcon Volt template with Semantic UI
Business Logic (Lib/):
AutoprovisionConf.php— ExtendsConfigClass. Registers workers, defines REST API routes, constants (BASE_URI = /pbxcore/api/autoprovision-http)Autoprovision.php— Core config generation. Routes to vendor-specific classes, sends SIP NOTIFY for phone reboot, AGI entry pointConfManager.php— Interface with single methodgenerateConfig($req_data, $sip_peers): stringAutoprovisionYealink.php,AutoprovisionSnom.php,AutoprovisionFanvil.php— Vendor-specific implementations ofConfManagerWorkerProvisioningServerPnP.php— Background worker: multicast PnP server, MAC filtering, device discoveryRestAPI/Controllers/GetController.php— REST API: config delivery, phonebook, device/user CRUD
Models (Phalcon ORM, Models/):
ModuleAutoprovision— Global settings (extension pattern, PBX host, MAC white/blacklists, additional INI params)ModuleAutoprovisionDevice— Phone devices (MAC, model, IP). Has manyUsersandBLFModuleAutoprovisionUsers— Maps users to device lines. Belongs toUsers(core) andDeviceModuleAutoprovisionBLF— BLF button configs per deviceTemplates,TemplatesUri,TemplatesUsers— Template system for config file generationOtherPBX— External PBX phonebook entries
All models extend ModulesModelsBase. Table names use m_ prefix (e.g., m_ModuleAutoprovisionDevice).
Setup:
Setup/PbxExtensionSetup.php— DB table creation, default data, extension registration. ExtendsPbxExtensionSetupBase
AGI:
agi-bin/ModuleAutoprovisionAGI.php— Asterisk AGI script triggered by dial pattern (e.g.,*2*XXXX). Resolves phone IP → MAC → device registration
- Source:
public/assets/js/src/module-autoprovision-index.js(ES6) - Compiled:
public/assets/js/module-autoprovision-index.js - Uses Semantic UI components, dynamic table management, AJAX form submission
Phones request configs via GET /pbxcore/api/autoprovision-http/* → AutoprovisionConf::moduleRestAPICallback() → GetController::getConfigStatic() → vendor-specific ConfManager::generateConfig().
- Create
Lib/AutoprovisionNewVendor.phpimplementingConfManagerinterface - Add MAC prefix detection in
GetController::getConfigStatic() - Add config generation routing in
Autoprovision::generateConfigPhone()
30 language files in Messages/. Key file: en.php. Translation keys prefixed with module_autoprovision_ or mod_autoprovision_.
The PBX is busybox/alpine — many "standard" Linux commands are absent or behave differently. Verified against MikoPBX 2026.2.89-dev on 172.16.32.94.
Two unrelated DBs are routinely confused:
/cf/conf/mikopbx.db— core MikoPBX (Extensions, Users, Sip, PbxExtensionModules). Them_ModuleAutoprovision*tables here are stale leftovers from older versions and ignored at runtime./storage/usbdisk1/mikopbx/custom_modules/ModuleAutoprovision/db/module.db— the live module DB. Allm_ModuleAutoprovision*,m_Templates*,m_OtherPBXreads/writes go here.
Phalcon's ModuleAutoprovision::findFirst() reads from module.db, so a sqlite3 /cf/conf/mikopbx.db UPDATE ... will appear to "work" yet have no effect. Always write via the Phalcon ORM, or hit /storage/.../module.db directly.
SystemMessages::sysLogMsg($ident, $message, $level) does not use $ident as the syslog tag. It logs through Phalcon's logger with the message body "$message on $ident", and the syslog tag ends up php.backend[<pid>] or php.frontend[<pid>]. Practical consequence:
- The
LOG_TAGconstants (autoprovision-pnpinWorkerProvisioningServerPnP,autoprovision-httpinGetController) appear at the end of the line, after" on ". grep autoprovision-pnp /storage/usbdisk1/mikopbx/log/system/messagesworks (substring match in body) — that's the canonical filter.
# PnP worker lifecycle and per-packet events
grep autoprovision-pnp /storage/usbdisk1/mikopbx/log/system/messages | tail -50
# HTTP delivery events (incoming requests, 200/404, vendor detection)
grep autoprovision-http /storage/usbdisk1/mikopbx/log/system/messages | tail -50
# Worker process
ps -ef | grep WorkerProvisioningServerPnP | grep -v grep
# Listening UDP ports — `ss` does NOT exist on MikoPBX, use netstat
netstat -ulnp | grep -E ':5060|:69 '
# Listening TCP ports (nginx must be on 8480 for provisioning HTTP)
netstat -ltnp | grep -E ':80|:8480|:443'
# Currently active module settings (live DB)
sqlite3 /storage/usbdisk1/mikopbx/custom_modules/ModuleAutoprovision/db/module.db \
-header 'SELECT * FROM m_ModuleAutoprovision;'WorkerProvisioningServerPnP::parseVerboseFlag() reads additional_params as an INI snippet:
[debug]
verbose = 1Update via Phalcon (raw sqlite3 /cf/conf/mikopbx.db UPDATE ... won't reach the worker — see "Data layout"):
php -r '
require_once "/usr/www/src/Core/Config/Globals.php";
use Modules\ModuleAutoprovision\Models\ModuleAutoprovision;
$d = ModuleAutoprovision::findFirst();
$d->additional_params = "[debug]\nverbose = 1\n";
$d->save();
'The worker reads settings only at start(); restart it: pkill -9 -f WorkerProvisioningServerPnP, then wait ~30 s for WorkerSafeScriptsCore to respawn it (SIGUSR1 alone reloads only some fields and is unreliable for additional_params). The next startup line should report verbose=1.
The worker ships its own client mode for end-to-end testing:
cd /storage/usbdisk1/mikopbx/custom_modules/ModuleAutoprovision/Lib
timeout 3 php -f WorkerProvisioningServerPnP.php \
socket_client <BIND_IP> <BIND_PORT> <MAC_NO_COLONS>
# example:
timeout 3 php -f WorkerProvisioningServerPnP.php socket_client 172.16.32.94 5063 805ec086888fIt binds to <BIND_IP>:<BIND_PORT> (must be free), sends a SUBSCRIBE to 224.0.1.75:5060, and prints the worker's 200 OK + NOTIFY response on stdout. The NOTIFY body contains the URL the real phone would fetch — inspect it to confirm {PBX_HOST} substitution and the chosen vendor/model.
Verbose logs land in /storage/usbdisk1/mikopbx/log/system/messages as: packet from <ip>:<port> method=SUBSCRIBE, then Request provisiong from ip: ...; mac=<r_mac>, then NOTIFY sent to <ip>:<port> mac=<mac> vendor=<v> model=<m>.
Caveat: r_mac is obtained via busybox arp -D <ip> and falls back to garbage like "in" (from "No match found in 4 entries") when ARP can't resolve the IP — common when testing from the PBX itself (its own IP isn't in its ARP table). The downstream code uses $headers['mac'] (parsed from the SUBSCRIBE URI), not r_mac, so this is cosmetic log noise.
Two paths are wired separately in GetController::getConfigStatic():
# Per-MAC path — substitutes {SIP_USER_NAME}/{SIP_NUM}/{SIP_PASS}/{PBX_HOST}/{FIRMWARE_URL}.
# Requires a row in m_TemplatesUsers binding the MAC to a templateId that exists in m_Templates.
curl -sv -A 'Yealink SIP-T19P 53.84.0.50' \
http://127.0.0.1:8480/pbxcore/api/autoprovision-http/<MAC>.cfg
# URI template path — substitutes only {PBX_HOST}/{FIRMWARE_URL}; {SIP_*} are left
# literal by design (operator warning logged as warn=unresolved-sip-placeholder).
curl -sv -A 'Snom 765/8.7.5.49' \
http://127.0.0.1:8480/pbxcore/api/autoprovision-http/snom-D785.htmTrap with URI templates: the route condition is ':uri: LIKE TemplatesUri.uri' — the column is the pattern, the request URI is the value. Phalcon receives the request URI with a leading slash (/snom-D785.htm); a m_TemplatesUri.uri value without that slash will never match. Entries should be stored either with leading slash or with a % wildcard.
- PnP NOTIFY arrives but URL is
http://:8480/...—m_ModuleAutoprovision.pbx_hostis empty. The renderer has no fallback for this; phones drop the malformed URL. Fill the "PBX host" field in the module settings before debugging anywhere else. - HTTP returns 200 but
{SIP_USER_NAME}/{SIP_NUM}survive verbatim in the config the phone downloads — the phone fetched a URI template (no MAC in URL), where per-user substitution is intentionally skipped. Either route the phone to its per-MAC URL via PnP, or accept that shared URI templates can't carry SIP credentials. - Asterisk logs
Request 'SUBSCRIBE' from '<sip:MAC...@224.0.1.75>' ... No matching endpoint found— benign pjsip rejection of the same multicast packetWorkerProvisioningServerPnPconsumes viaSOCK_RAW+MCAST_JOIN_GROUP. Not evidence the worker missed it. Cross-checkgrep autoprovision-pnp .../messagesfor the matchingpacket from ...line under verbose. grep autoprovision-pnpreturns nothing even though the worker is inps— verbose is off and no SUBSCRIBE packets have arrived since the last startup. Lifecycle lines (PnP listener starting,Joined multicast group,PnP listener ready) are emitted only on startup; trigger a restart to confirm the worker is logging at all.- Per-MAC HTTP returns 404 — either no
m_TemplatesUsersrow for that MAC, orm_TemplatesUsers.templateIdpoints to a missing row inm_Templates. Check both joins manually.