Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
94b9c9f
Исправил отображение номера собеседника при звонках через цепочку Loc…
Feb 3, 2026
1de336a
Исправил определение src_chan при collectActiveChannels и бесконечный…
Feb 3, 2026
17f85d6
Заменил CHECK_BY_AMI на state-file контроль здоровья WorkerActiveCalls
Feb 3, 2026
f700490
Расширен getUsersStates: добавлено направление звонка и состояние оче…
Feb 4, 2026
33e6d23
Исправлено удаление завершённых звонков при изменении Linkedid
Feb 4, 2026
b3f347b
Реализована ФАЗА 1 оптимизации WebSocket обновлений
Feb 5, 2026
91126c9
Исправлен throttle: добавлена принудительная отправка обновлений чере…
Feb 5, 2026
12a80a0
Уменьшен socket timeout для быстрой отправки WebSocket обновлений
Feb 5, 2026
aee33d3
Уменьшен socket timeout с 1 сек до 300ms для минимальной латентности
Feb 5, 2026
dca0a87
Исправлена проблема с src_chan при параллельном вызове нескольких уст…
Feb 5, 2026
7a5c0a0
Исправлена проблема с отслеживанием перехваченных звонков (Call Picku…
Feb 9, 2026
c8873c4
Исправлена проблема с пропаданием собеседника после Call Pickup (*8XXX)
Feb 9, 2026
9e6548a
Полное исправление Call Pickup: миграция метаданных и code cleanup
Feb 9, 2026
bf17fc7
Безопасность токенов: сервисный токен + refresh endpoint
Feb 14, 2026
c63aaa3
Исправлена ошибка stream_set_timeout на невалидном сокете AMI
Feb 16, 2026
065a17a
Устранение зависаний WorkerAmiActions: reconnect по требованию, защит…
Feb 18, 2026
3943c9a
Периодический re-publish в nchan для новых подписчиков при отсутствии…
Feb 22, 2026
fdc725c
Исправление зависания каналов после перезагрузки Asterisk
Mar 31, 2026
73c4447
Обобщённая миграция Linkedid при BridgeEnter (interception-bridge)
Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions App/Controllers/ModuleMonitorActiveCallsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,9 @@ public function saveAction() :void
public function backandEnableAction() :void
{
if (MonitorActiveCallsMain::backendExists()) {
// Модуль включен.
$api = new ModuleSoftphoneBackendApi();
$api->initialize();
$this->view->data = $api->createLoginResponse('1', 'admin');
$this->view->success = true;
$result = ModuleSoftphoneBackendApi::createServiceToken('ModuleMonitorActiveCalls');
$this->view->data = $result;
$this->view->success = !empty($result['success']);
}else{
$this->view->success = false;
$this->view->data = [];
Expand Down
233 changes: 233 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## КРИТИЧЕСКИ ВАЖНО: Запреты при разработке

**КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО:**
1. **Изменять файлы вне директории модуля** - никогда не модифицировать файлы в `/offload/`, `/usr/www/src/`, или других системных директориях MikoPBX
2. **Использовать rsync, cp -r или tar для установки модуля** - это может перезаписать системные файлы MikoPBX
3. **Удалять директорию модуля целиком** (`rm -rf /storage/.../ModuleMonitorActiveCalls`) - это удалит базу данных модуля

## Требования к коду

**Совместимость:**
- Код должен быть совместим с **PHP 7.4** и **PHP 8.x**
- Код должен работать с **Phalcon 4.x** и **Phalcon 5.x** (используйте `MikoPBXVersion`)

**Избегайте (только PHP 8+):**
- `match` выражения → используйте `switch`
- Union types `function foo(): int|string` → используйте PHPDoc
- Named arguments `foo(name: $value)`
- Constructor property promotion
- Nullsafe operator `?->`
- `str_contains()`, `str_starts_with()`, `str_ends_with()` → используйте `strpos() !== false`

## Project Overview

ModuleMonitorActiveCalls is a PHP module for MikoPBX (Asterisk-based VoIP PBX) that monitors active calls and enables supervisors to perform real-time call actions (listen, whisper, barge-in, hangup).

**Stack:** PHP 7.4.6+, Phalcon MVC framework, Redis cache, Asterisk AMI, Vue.js frontend

## Сборка и установка модуля

### Сборка архива (локально)

```bash
cd /Volumes/DevDisk/apor/Developement/MikoPBX/Extensions/ModuleMonitorActiveCalls
zip -r ../ModuleMonitorActiveCalls.zip . -x "*.git*" -x "*tasks.md*" -x "*.DS_Store*" -x "*CLAUDE.md*"
```

### Загрузка на сервер

```bash
scp ../ModuleMonitorActiveCalls.zip user@server:/home/user/
```

### Установка через WorkerModuleInstaller (ЕДИНСТВЕННЫЙ РАЗРЕШЁННЫЙ СПОСОБ)

```bash
# На сервере: создать settings.json
cat > /tmp/settings.json << 'EOF'
{
"currentModuleDir": "/storage/usbdisk1/mikopbx/custom_modules/ModuleMonitorActiveCalls",
"filePath": "/home/user/ModuleMonitorActiveCalls.zip",
"uniqid": "ModuleMonitorActiveCalls"
}
EOF

# Установить модуль (сохраняет БД!)
php -f /usr/www/src/PBXCoreREST/Workers/WorkerModuleInstaller.php start /tmp/settings.json
```

### Быстрая команда (всё в одном)

```bash
cd /Volumes/DevDisk/apor/Developement/MikoPBX/Extensions/ModuleMonitorActiveCalls && \
zip -r ../ModuleMonitorActiveCalls.zip . -x "*.git*" -x "*tasks.md*" -x "*.DS_Store*" -x "*CLAUDE.md*" && \
scp ../ModuleMonitorActiveCalls.zip user@server:/home/user/ && \
ssh user@server 'php -f /usr/www/src/PBXCoreREST/Workers/WorkerModuleInstaller.php start /tmp/settings.json'
```

## Инициализация в скриптах

Все PHP скрипты (bin/, tests/) должны начинаться с:

```php
#!/usr/bin/php
<?php
require_once('Globals.php');

use Modules\ModuleMonitorActiveCalls\Models\ModuleMonitorActiveCalls;
// ...
```

**Важно:** `Globals.php` должен быть симлинком на `/usr/www/src/Core/Config/Globals.php`

```bash
ln -sf /usr/www/src/Core/Config/Globals.php /storage/.../ModuleMonitorActiveCalls/bin/Globals.php
```

## Architecture

### Dual-Worker Pattern

1. **WorkerActiveCalls** (`bin/WorkerActiveCalls.php`) - Main AMI event listener
- Connects to Asterisk Manager Interface
- Listens for call/queue events (Newchannel, Hangup, BridgeEnter, QueueCallerJoin, etc.)
- Maintains state for channels, bridges, queue entries
- Writes active call data to Redis cache

2. **WorkerAmiActions** (`bin/WorkerAmiActions.php`) - Call action executor
- Consumes Beanstalk queue messages
- Executes call actions (Hangup, Listen, Whisper, Join)
- Rate-limited AMI operations

3. **safe.php** (`bin/safe.php`) - Process keeper (runs via cron every minute)

### Worker Health Check (State-file подход)

WorkerActiveCalls использует **собственный механизм контроля здоровья** вместо стандартного `CHECK_BY_AMI` из MikoPBX core.

**Проблема CHECK_BY_AMI:** WorkerSafeScriptsCore пингует воркер через AMI UserEvent и ожидает pong. Во время тяжёлой инициализации (`collectActiveChannels` делает 7-8 AMI GetVar на каждый активный канал) воркер не находится в event loop и не может ответить на ping. Safe scripts считают воркер зависшим и отправляют SIGUSR1 (restart). Новый воркер тоже не успевает ответить на ping во время init — бесконечный цикл рестартов каждые ~3 минуты.

**Решение — state-file + idle callback:**

1. **Тип проверки:** `CHECK_BY_PID_NOT_ALERT` — MikoPBX core только проверяет PID, не пингует через AMI
2. **State-файл** (`/tmp/MonitorActiveCalls_worker.state`):
```json
{"pid": 12345, "ts": 1738610800, "status": "running"}
```
3. **Жизненный цикл обновлений:**
- `start()` → `status=starting` (перед инициализацией)
- После init → `status=running`
- В event loop → idle callback обновляет `ts` каждые 30 сек
4. **Idle callback в AsteriskManager:**
- `waitUserEvent()` имеет внутренний do-while цикл с socket timeout 5 сек
- При timeout + успешный AMI ping (соединение живо) + прошло ≥30 сек → вызов callback
- Callback обновляет state-файл — доказывает что процесс жив И AMI-соединение работает
5. **safe.php** (cron каждую минуту) проверяет state-файл:
- Нет PID → запустить
- State-файл отсутствует/повреждён → kill + restart
- PID в файле ≠ реальному PID → kill + restart
- `status=starting`, age > 120 сек → зависла инициализация, kill + restart
- `status=running`, age > 90 сек → воркер завис, kill + restart
- Иначе → всё ок

### Data Flow

```
Asterisk AMI Events → WorkerActiveCalls → Redis Cache → Web UI (reads cache)
User Action → Controller → Beanstalk Queue → WorkerAmiActions → Asterisk AMI
```

### Key Files

| File | Purpose |
|------|---------|
| `Lib/MonitorActiveCallsConf.php` | Module configuration, worker definitions, AMI config |
| `Lib/MonitorActiveCallsMain.php` | Module lifecycle (start/stop workers) |
| `Lib/AsteriskManager.php` | Custom AMI socket client |
| `Lib/CacheManager.php` | Redis adapter (DB index 3, prefix `ModuleMonitorActiveCalls_`) |
| `App/Controllers/ModuleMonitorActiveCallsController.php` | Web API endpoints |
| `public/assets/js/module-monitor-active-calls-index.js` | Vue.js frontend |

### Database Models

- **ModuleMonitorActiveCalls** - Module settings (admin user ID)
- **UsersSettings** - Per-user preferences (queue filters, min wait visible)

Tables are created via Phalcon model annotations in `Setup/PbxExtensionSetup.php`.

### API Endpoints

- `getActiveChannelsAction()` - Fetch active calls (v1)
- `getActiveChannelsV2Action()` - Fetch active calls with queues (v2)
- `executeCallAction()` - Execute call action (hangup/listen/whisper/join)
- `saveUserAction()` - Save user settings

### Access Control

Integrates with optional `ModuleUsersUI` module for role-based filtering. Supervisors see all calls; agents see only their extensions.

## Channel States

Constants in `WorkerActiveCalls`:
- `STATE_IDLE`, `STATE_RINGING`, `STATE_ONHOLD`, `STATE_UP`, `STATE_BUSY`, `STATE_UNAVAILABLE`

## Configuration

AMI user `monitor-active-calls` is auto-generated with limited permissions (read: system,agent,call,cdr,user; write: system,agent,call,originate).

## File Paths (на сервере)

| Path | Description |
|------|-------------|
| `/storage/usbdisk1/mikopbx/custom_modules/ModuleMonitorActiveCalls/` | Module directory |
| `/storage/usbdisk1/mikopbx/custom_modules/ModuleMonitorActiveCalls/db/module.db` | SQLite database |
| `/storage/usbdisk1/mikopbx/logs/ModuleMonitorActiveCalls/` | Log files |
| `/usr/www/src/Core/Config/Globals.php` | MikoPBX bootstrap |

## Совместимость с Phalcon

Используйте `MikoPBXVersion` для кросс-версионной совместимости:

| Метод | Phalcon 4 | Phalcon 5 |
|-------|-----------|-----------|
| `isPhalcon5Version()` | `false` | `true` |
| `getDefaultDi()` | `\Phalcon\Di::getDefault()` | `\Phalcon\Di\Di::getDefault()` |
| `getLoggerClass()` | `\Phalcon\Logger` | `\Phalcon\Logger\Logger` |

## Translations

28 languages supported in `/Messages/` directory (PHP arrays).

## Dependencies

- **Required:** MikoPBX >= 2024.1.114
- **Optional:** ModuleUsersUI (access control), ModuleSoftphoneBackend (contact names)
- **PHP:** cesargb/php-log-rotation 2.6.0

## JavaScript Build

Source files are in `public/assets/js/src/`. After modifying them, compiled files must be generated in `public/assets/js/`.

**IMPORTANT:** Only edit files in `src/` directory. Files in `public/assets/js/*.js` are auto-generated.

Build process uses Babel via PHPStorm File Watcher:
- See setup: https://docs.mikopbx.com/mikopbx-development/prepare-ide-tools/mac#phpstorm-setup-babel
- Babel path: `/Users/apor/Developement/MikoPBX/MikoPBXUtils/node_modules/.bin/babel`
- Presets: `airbnb`
- Source maps: enabled

To rebuild manually:
```bash
cd /Users/apor/Developement/MikoPBX/MikoPBXUtils && \
cp babel.config.json babel.config.json.bak && \
echo '{"presets":[["@babel/preset-env",{"targets":{"chrome":50,"ie":11,"firefox":45}}]]}' > babel.config.json && \
./node_modules/.bin/babel \
/Volumes/DevDisk/apor/Developement/MikoPBX/Extensions/ModuleMonitorActiveCalls/public/assets/js/src/module-monitor-active-calls-index.js \
--out-dir /Volumes/DevDisk/apor/Developement/MikoPBX/Extensions/ModuleMonitorActiveCalls/public/assets/js/ \
--source-maps && \
mv babel.config.json.bak babel.config.json
```
49 changes: 47 additions & 2 deletions Lib/AsteriskManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ class AsteriskManager
*/
private bool $_loggedIn = false;

/** @var callable|null Callback invoked periodically when AMI connection is alive and idle */
private $onIdleCallback = null;

/** @var int Timestamp of last idle callback invocation */
private int $lastIdleCall = 0;

/** @var int Minimum interval between idle callback calls (seconds) */
private int $idleInterval = 30;

/**
* Constructor
*
Expand Down Expand Up @@ -552,6 +561,34 @@ public function ping(): array
return $this->sendRequestTimeout('Ping');
}

/**
* Sets socket read timeout.
*
* @param int $seconds Timeout in seconds
* @param int $microseconds Timeout in microseconds (optional)
* @return bool True if socket is valid and timeout was set
*/
public function setSocketTimeout(int $seconds, int $microseconds = 0): bool
{
if (!is_resource($this->socket)) {
return false;
}
return stream_set_timeout($this->socket, $seconds, $microseconds);
}

/**
* Wait for a user events.
*
* @param $allow_timeout bool
*
* @return array of parameters, empty on timeout
*/
public function setOnIdleCallback(callable $callback, int $interval = 30): void
{
$this->onIdleCallback = $callback;
$this->idleInterval = $interval;
}

/**
* Wait for a user events.
*
Expand All @@ -578,6 +615,11 @@ public function waitUserEvent(bool $allow_timeout = false): array
}
if ($type === '' && count($this->ping()) === 0) {
$timeout = $allow_timeout;
} elseif ($type === '' && $this->onIdleCallback !== null
&& time() - $this->lastIdleCall >= $this->idleInterval
) {
$this->lastIdleCall = time();
call_user_func($this->onIdleCallback);
} elseif (stripos($type, 'event') !== false) {
$this->processEvent($parameters);
}
Expand Down Expand Up @@ -813,11 +855,14 @@ public function ExtensionState(string $exten, string $context, ?string $actionid
* Get the channels information.
*
* @param bool $group Indicates whether to group the channels by Linkedid (optional, default is true).
* @return array The channels information.
* @return array|null The channels information, or null on AMI communication error.
*/
public function GetChannels(bool $group = true): array
public function GetChannels(bool $group = true)
{
$res = $this->sendRequestTimeout('CoreShowChannels');
if (empty($res)) {
return null;
}
$channels = null;
if (isset($res['data']['CoreShowChannel'])) {
$channels = $res['data']['CoreShowChannel'];
Expand Down
2 changes: 1 addition & 1 deletion Lib/MonitorActiveCallsConf.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function getModuleWorkers(): array
'worker' => WorkerAmiActions::class,
],
[
'type' => WorkerSafeScriptsCore::CHECK_BY_AMI,
'type' => WorkerSafeScriptsCore::CHECK_BY_PID_NOT_ALERT,
'worker' => WorkerActiveCalls::class,
],
];
Expand Down
Loading
Loading